
简介一套基于Django框架的毕业生就业管理系统完整源码适用于计算机毕业设计、课程设计及就业管理业务学习。系统采用Python语言开发后端使用Django数据库采用MySQL以浏览器/服务器模式提供后台管理功能。内设管理员、教师、学生三类角色管理员可维护学院、教师、学生信息支持表格批量导入学生发布公告并查看按学院统计的就业数据及就业率排行教师可审核学生实习资料、登记就业状态并跟踪未就业学生一年内的动态学生可提交实习材料、查看审核与留言。资源共2000个文件主体为页面、脚本、样式、Python源码及SQL脚本另有说明文档和答辩PPT压缩包约22.75MB部署后可正常运行。当前已有86人学习下载适合需要完整项目参考、快速上手Django开发与就业管理系统设计的读者。1. 统计口径混乱的就业管理系统问题从来不在 SQL做就业管理系统的难点通常不在功能多寡而在统计口径。一个学生考研成功算不算就业实习审核通过但未登记去向在报表里是归入“未就业”还是“待就业”如果这些口径在数据库层面没有显式字段约束几张统计报表很容易各算各的最后学院领导和就业办拿到的数字对不上。这套基于 Django MySQL 的毕业生就业管理系统把这个问题拆成了三个层次解决数据模型层用字段枚举固定状态业务层用三个角色管理员、教师、学生的权限边界约束操作展示层用 ECharts 按统一口径出图。适合两类人看一是正在做 Python 课程设计/毕业设计、需要完整技术参照的学生二是想了解 Django 后台管理系统如何组织多角色权限与统计报表的后端开发者。下面从数据模型和权限设计讲起再落到 Excel 导入、实习审核、统计图表这些关键功能的实现细节。2. 数据模型与权限边界三张用户表和一个中间件的设计2.1 用户模型的三种方案对比Django 自带auth.User但就业管理系统有管理员、教师、学生三个身份直接用默认 User 表加is_staff标记会面临一个典型问题学生和教师需要关联到学院而 Django 默认的 User 表没有学院外键强行加user_profile扩展表会多一层查询。常见做法有三种实际项目里多数人按情况选方案做法适用场景本系统选择扩展 Profile新建表通过 OneToOne 关联 auth.User需要复用 Django Admin 且角色少否自定义 User继承 AbstractUser增加角色字段角色多且业务差异大是角色即外键User 表加 teacher_id、student_id 可空外键从旧系统迁移保留账号体系否这套系统用的是第二种核心是自定义用户模型在models.py里继承AbstractUser加入角色字段和学院外键from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): 自定义用户表替代默认 auth.User ROLE_CHOICES ( (1, 管理员), (2, 教师), (3, 学生), ) role models.SmallIntegerField(choicesROLE_CHOICES, default3, verbose_name角色) college models.ForeignKey(College, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name所属学院) class Meta: db_table sys_user verbose_name 用户注意三个细节role字段用 SmallIntegerField 而不是 CharField查询时走整型索引更快college外键用SET_NULL学院被删除时用户记录还能保留避免误删db_table显式指定表名方便和 py 脚本里已有的 SQL 对齐。如果你是在已有库上改造db_table不建议随意改否则 migrations 会报表已存在。2.2 中间件实现三套权限拦截三个角色的菜单和操作入口完全不同如果只在模板里做{% if user.role 1 %}这种判断前端隐藏了但 URL 还能直接访问后端必须做二次校验。用 Django 中间件统一拦截是最省事的路子在middleware.py里写一个角色路由校验# middleware.py from django.shortcuts import redirect from django.urls import resolve class RoleBasedAccessMiddleware: 按角色拦截未授权访问 ALLOWED_URL_PREFIX { 1: [/admin/, /manage/], # 管理员可访问前缀 2: [/teacher/, /notice/], # 教师可访问前缀 3: [/student/, /notice/], # 学生可访问前缀 } def __init__(self, get_response): self.get_response get_response def __call__(self, request): if not request.user.is_authenticated: return self.get_response(request) path request.path allowed_prefixes self.ALLOWED_URL_PREFIX.get(request.user.role, []) # 静态资源和登录页放行 if path.startswith(/static/) or path /login/: return self.get_response(request) if not any(path.startswith(p) for p in allowed_prefixes): return redirect(/login/?next path) return self.get_response(request)写中间件时有几个容易踩的坑一是__init__里不能写任何依赖 request 的逻辑因为它在 Django 启动时就执行了二是要放行静态文件和登录接口否则页面登录完成后又被中间件踢回去形成重定向循环三是前缀匹配要用startswith直接比path /teacher/会把子路由全部挡掉。3. Excel 批量导入与实习审核两个最容易被问到的功能3.1 openpyxl 导入学生信息的完整流程管理员按学院批量导入学生信息是系统里实务价值最高的功能之一。毕业生就业管理系统每年要从教务系统导出上千条学生数据逐条手工录入不现实。这里的实现用 openpyxl 读取 .xlsx逐行校验后批量写入事务包裹保证不产生半截数据# views.py管理员导入功能 import openpyxl from django.db import transaction from django.contrib import messages def import_students(request): if request.method POST: excel_file request.FILES.get(excel_file) if not excel_file: messages.error(request, 请选择要上传的 Excel 文件) return redirect(/manage/student/import/) wb openpyxl.load_workbook(excel_file, data_onlyTrue) sheet wb.active success_count 0 error_rows [] with transaction.atomic(): # 保证批量导入要么全成功要么全回滚 for row in sheet.iter_rows(min_row2, values_onlyTrue): # 约定 Excel 列顺序学号、姓名、学院、专业、班级、性别、手机号 student_no, name, college_name, major, clazz, gender_str, phone row[:7] if not student_no or not name: error_rows.append(row) continue college, _ College.objects.get_or_create(namecollege_name) # get_or_create 避免重复导入时外键冲突 Student.objects.create( student_nostr(student_no).strip(), namename.strip(), collegecollege, majormajor.strip() if major else , clazzclazz.strip() if clazz else , gender1 if gender_str 男 else 0, phonestr(phone).strip() if phone else ) success_count 1 messages.success(request, f成功导入 {success_count} 条跳过 {len(error_rows)} 条异常数据) return redirect(/manage/student/)用transaction.atomic()包住整段循环是刻意为之因为学生信息导入一半时如果报错数据库里会出现“同一批新生只有部分在库”的脏状态后续就业统计全偏。日常维护时如果你只是想看哪些行有问题可以去掉事务逐行提交但正式功能里我倾向保留事务。3.2 实习审核的状态机设计教师审核学生提交的实习材料表面是一个“通过/驳回”操作实际涉及状态流转。学生提交实习资料后状态是“待审核”教师可以审核通过或驳回并留言学生看到结果后可以重新提交。实现上不写 if-else 金字塔用状态字段加字典映射# models.py 实习状态字段 class Internship(models.Model): STATUS_CHOICES ( (0, 待审核), (1, 已通过), (2, 已驳回), ) student models.ForeignKey(Student, on_deletemodels.CASCADE, verbose_name学生) company_name models.CharField(max_length100, verbose_name实习单位) start_date models.DateField(verbose_name实习开始时间) end_date models.DateField(verbose_name实习结束时间) attachment models.FileField(upload_tointernship/, verbose_name实习材料) teacher_comment models.TextField(blankTrue, verbose_name教师留言) status models.SmallIntegerField(choicesSTATUS_CHOICES, default0, verbose_name审核状态)审核时教师只更新两个字段status和teacher_comment。有一个值得注意的细节学生重新提交时应该清空旧的教师留言和状态否则会出现“状态是待审核但留言还是上次驳回内容”的残留信息。在学生的重新提交视图里要主动重置def resubmit_internship(request, pk): internship Internship.objects.get(pkpk, studentrequest.user.student) internship.status 0 internship.teacher_comment internship.attachment request.FILES.get(attachment) internship.save()这步虽然简单但很多同类系统都会漏掉最后数据呈现出来逻辑混乱排查起来又费时间。3.3 SQL 与 ORM 在统计场景的取舍Django ORM 对常规 CRUD 很方便但就业统计涉及“按学院分组、多表聚合”ORM 写出来又长又难调。把 ORM 和原生 SQL 混着用是更务实的方案。系统里学院就业率排行先用 ORM 查出就业状态分布再在 Python 侧算比率因为最终要渲染成 ECharts 的柱状图和饼状图数据结构本来就是嵌套列表不用非得在 SQL 里拼好from django.db.models import Count def college_employment_stats(request): 按学院统计已就业/未就业/考研人数 colleges College.objects.annotate( employedCount(student, filterQ(student__employment_status1)), unemployedCount(student, filterQ(student__employment_status0)), postgraduateCount(student, filterQ(student__employment_status2)), ) data [] for c in colleges: total c.employed c.unemployed c.postgraduate rate round(c.employed / total * 100, 2) if total 0 else 0 data.append({ name: c.name, employed: c.employed, unemployed: c.unemployed, postgraduate: c.postgraduate, rate: rate }) return JsonResponse({data: data})这里用Q对象做条件聚合一次查询拿到三种状态的人数避免循环里反复查库造成 N1。annotate生成的 SQL 会在GROUP BY后做条件统计比分别跑三次COUNT效率高。4. 就业统计图表的实现从 COUNT 到 ECharts 的完整链路4.1 后端接口返回 JSON 数据结构统计页面前后端的数据约定决定开发时两边能不能并行。图表组件要求的数据结构是“名字 数值”的数组接口直接按这个结构返回前端就不用二次重组。以就业状态饼图为例后端组装数据# views.py - 提供 ECharts 饼图数据 def employment_pie_data(request): 返回全校就业状态分布用于饼图渲染 # 就业状态0 未就业1 已就业2 考研 result Student.objects.values(employment_status) \ .annotate(countCount(id)) status_map {0: 未就业, 1: 已就业, 2: 考研} pie_data [] for item in result: status item[employment_status] pie_data.append({ name: status_map.get(status, 未知), value: item[count] }) return JsonResponse({code: 0, data: pie_data})注意values(...).annotate(...)的分组顺序如果先annotate后values聚合结果会按模型默认排序分组结果经常和你预期不一致。Django 的文档里明确写在values()之后调用annotate()才能按指定字段分组。4.2 前端 ECharts 渲染与刷新图表展示部分用 ECharts 的 CDN 引入即可不需要 npm 工程化。页面加载时用 fetch 请求后端接口拿到数据后setOption直接渲染// static/js/employment_chart.js fetch(/api/employment/pie/) .then(res res.json()) .then(res { if (res.code 0) { const chart echarts.init(document.getElementById(pieChart)); chart.setOption({ title: { text: 全校就业状态分布, left: center }, tooltip: { trigger: item }, legend: { bottom: 0 }, series: [{ type: pie, radius: 60%, data: res.data }] }); window.addEventListener(resize, () chart.resize()); } });考试时经常考到的 ECharts 参数有radius饼图内径设为 60% 是环形效果、trigger提示框触发方式item 是按数据项触发axis 是按坐标轴触发。柱状图的排行逻辑类似把type换成barxAxis.data填学院名yAxis用数值轴再配合label显示就业率百分比barOption { xAxis: { type: category, data: res.data.map(d d.name) }, yAxis: { type: value, max: 100, axisLabel: { formatter: {value}% } }, series: [{ type: bar, data: res.data.map(d d.rate), label: { show: true, position: top, formatter: {c}% }, itemStyle: { color: function(params) { const colors [#c23531, #2f4554, #61a0a8, #d48265]; return colors[params.dataIndex % colors.length]; } } }] };itemStyle.color写成函数而不是固定值是为了排名前三的学院用高亮色后面学院用淡色这类细节在答辩演示时很讨喜。formatter: {c}%控制柱顶标签显示数值加百分号否则默认不带单位。4.3 未就业跟踪的查询优化未就业学生跟踪是教师角色的核心功能要求教师能登记学生毕业后一年内的就业动向。如果一个学生有多条跟踪记录列表页默认展示最新一条直接order_by(-create_time).first()在循环里会产生 N1 查询。更好的方式是用窗口函数但 Django ORM 对窗口函数支持有限这里用子查询解决# views.py - 未就业学生列表带最新跟踪记录 from django.db.models import OuterRef, Subquery def unemployed_students(request): # 最新跟踪记录子查询 latest_track StudentTrack.objects.filter( studentOuterRef(pk) ).order_by(-create_time)[:1] students Student.objects.filter( employment_status0 ).annotate( latest_noteSubquery(latest_track.values(note)[:1]), latest_timeSubquery(latest_track.values(create_time)[:1]) ) return render(request, teacher/unemployed.html, {students: students})Subquery(latest_track.values(note)[:1])是 Django 1.11 之后的标准写子查询方式注意latest_track里已经order_by(-create_time)取了第一条外层values(note)[:1]是固定取一条。这套写法的好处是列表页一次查询到位数据量大时也不会卡。5. 部署后必查的三处配置时区、CSRF 和静态文件收集把系统跑起来只是第一步部署到 Linux 服务器上才是真正的分水岭。很多学生在本地 dev server 跑得欢一换环境就崩问题往往不是代码逻辑而是 Django 项目里几个容易忽略的配置项。5.1 时区与存储引擎Django 默认USE_TZ True所有DateTimeField写入数据库时按 UTC 存。本地开发时没感觉部署到国内服务器后所有时间凭空少 8 小时。在settings.py里做两处修改# settings.py TIME_ZONE Asia/Shanghai USE_TZ TrueUSE_TZ不建议设为 False因为 Django admin 和第三方库很多地方依赖时区换算。保持 True 的情况下只需要确保写入数据库前把datetime.now()换成timezone.now()from django.utils import timezone now timezone.now() # 不要用 datetime.now()MySQL 的存储引擎也值得检查。如果还在用 MyISAM建议迁移到 InnoDB虽然 MyISAM 查询快但不支持事务和外键而这个系统里学生和学院有关联、实习记录和教师有关联外键约束是数据库层面的最后一道防线。迁移命令一行ALTER TABLE sys_user ENGINEInnoDB;5.2 CSRF 与跨域问题Django 的 CSRF 防护默认开启模板里用{% csrf_token %}没问题但如果你把前后端拆开了比如 Django 只提供 JSON API前端页面是单独的 Vue 项目就要在接口上做两处适配from django.views.decorators.csrf import csrf_exempt csrf_exempt def api_employment_stats(request): 对外统计接口跨域场景下豁免 CSRF 校验 ...用csrf_exempt豁免 CSRF 之前先确认这个接口是否真的被第三方系统调用。如果只是同源站点内嵌保留 CSRF 校验更安全。跨域时 Django 会拦掉带 Authorization 头的预检请求还需要加django-cors-headers并在settings.py里配置CORS_ALLOWED_ORIGINS。5.3 静态文件收集开发阶段debugTrueDjango 自己托管静态文件。部署后配 Nginx 时要把所有静态文件收到一个目录否则页面样式全部丢光。在settings.py里配置STATIC_URL /static/ STATIC_ROOT os.path.join(BASE_DIR, staticfiles)然后执行python3 manage.py collectstaticcollectstatic会把所有 app 下的 static 目录和项目公共 static 目录的文件复制到STATIC_ROOT里Nginx 直接指向这个目录即可。有个很常见的坑layui.css、bootstrap.min.css 这些第三方库放在项目根目录的 static 下但STATICFILES_DIRS没配collectstatic 时这些文件不会被收集。所以检查一下STATICFILES_DIRS [ os.path.join(BASE_DIR, static), # 项目公共 static 目录 ]配好后重新执行 collectstatic再用浏览器无痕模式打开页面确认 CSS 和 JS 都加载成功。如果样式还是乱打开浏览器开发者工具看 Console 里的 404 路径直接就知道是哪个静态文件路径没对上。本文还有配套的精品资源点击获取