
每年到了毕业季学校就业指导中心、二级学院辅导员、甚至学生自己都会被一堆就业数据搞得焦头烂额。就业率怎么算、哪个专业薪资高、毕业生都去了哪些行业和城市、哪些因素真正影响了起薪——这些问题的答案其实都埋藏在教务系统、就业系统的原始数据里问题在于没人把这些数据真正“挖”出来。我去年做了一个“DjangoVue 基于机器学习的毕业生就业数据分析与可视化系统”核心就是用Django搭建后端数据处理与算法接口用Vue和ECharts做前端可视化看板再让机器学习模型去挖掘数据里隐藏的规律。这套系统跑通之后就业数据的价值才算真正被释放出来了。这个项目特别适合三类人参考一是正在做毕业设计、需要完整系统的计算机专业学生二是高校信息化部门或就业办的技术人员想把手头数据变成管理决策依据三是想理解前后端分离项目如何与机器学习算法结合的同学。今天我把整个系统的设计思路、核心代码实现、踩过的坑全部拆开讲一遍内容偏实操你照着搭就能跑通。1. 系统整体设计与技术选型为什么非要用 Django Vue1.1 技术选型背后的底层逻辑凡是涉及“数据分析”的管理系统市面上常见的组合有 Spring Boot Vue、Flask ECharts、Django 自研模板等。我最终选择 Django Vue 并不是因为跟风而是从三类需求出发做的判断。第一个需求是算法集成要顺。机器学习模型训练完要对外提供服务Django 天生适合干这件事。它的 ORM 能直接把数据库里的学生就业记录查出来转成 DataFrame模型推理结果也能轻松写成 JSON 接口返回给前端不需要额外搭一层服务。第二个需求是后台管理要快。就业数据的录入、清洗、审核、导出这些脏活累活靠 Django 自带 admin 能省出大量开发时间。第三个需求是前端可视化要灵活。Vue 单文件组件配合 ECharts比 Django 模板渲染图表灵活太多——模板渲染是后端拼 HTML每次切换图表维度都要刷新页面而 Vue 是数据驱动视图切换筛选条件只改数据不改结构交互体验完全是两个级别。一句话总结Django 负责“管数据”和“跑算法”Vue 负责“看数据”各干各最擅长的事。1.2 前后端分离的系统架构具体怎么分整个项目采用前后端完全分离的架构前端工程和后端工程是两个独立目录部署时也各自独立运行。后端只提供 RESTful API不返回任何 HTML 页面前端所有页面通过 Axios 调用后端接口获取 JSON 数据然后用 ECharts 渲染图表。这套架构的核心是“通过接口契约协作”具体落地时我用 Django REST Framework 来生成 RESTful API。项目目录大致长这样就业分析系统/ ├── backend/ │ ├── manage.py │ ├── config/ # Django 项目配置(settings/urls) │ ├── apps/ │ │ ├── users/ # 用户认证模块 │ │ ├── students/ # 学生信息管理模块 │ │ ├── employment/ # 就业信息管理模块 │ │ └── analysis/ # 数据分析与机器学习模块 │ └── data/ # 原始数据集与预处理脚本 ├── frontend/ │ ├── src/ │ │ ├── api/ # Axios 接口封装 │ │ ├── views/ │ │ │ ├── Dashboard.vue # 可视化大屏 │ │ │ ├── StudentList.vue # 学生列表 │ │ │ ├── Analysis.vue # 机器学习分析页 │ │ │ └── Login.vue # 登录页 │ │ ├── components/ # ECharts 图表封装组件 │ │ ├── router/ # 前端路由 │ │ └── store/ # 状态管理 │ └── package.json └── README.md需要提醒的是开发阶段前端用 Vue CLI 或 Vite 自带的热更新服务端口 8080后端 Django 跑在 8000必须配置跨域。我在 Django 的 settings.py 里加了django-cors-headers这个库然后设置CORS_ALLOW_ALL_ORIGINS True开发省心但上线前要收紧为白名单。1.3 机器学习模块在系统里到底扮演什么角色先泼一盆冷水这个系统里机器学习不是炫技用的它的价值体现在三个真正能落地的场景上。第一个场景是“专业—薪资”的回归预测。我用毕业生的 GPA、实习时长、技能证书数量、专业类别这些特征训练一个多元线性回归或随机森林回归模型预测某个学生的期望起薪范围。第二个场景是“就业去向”的聚类分析。K-Means 聚类把毕业生分成几个典型群体比如“考研深造型”“互联网高薪型”“稳定就业型”“灵活就业型”就业办可以针对不同群体制定差异化的就业指导策略。第三个场景是“是否就业”的二元分类。用逻辑回归或决策树分析学生特征对就业结果的影响顺带输出特征重要性告诉管理者到底哪些因素对就业影响最大。这里有一个关键认知算法不是越复杂越好。就业数据特征维度一般不超过 30 个数据量也就是几千到几万条量级XGBoost、深度学习这类模型反而容易过拟合且难以解释。我在项目里用线性回归、K-Means、决策树居多真正跑起来后就业办老师更看重的是“为什么”而不是“准确率 99%”。2. 数据库设计把就业数据的业务模型一次讲透2.1 核心数据表结构设计数据库是整个系统的地基。我设计了 5 张核心业务表外加 Django 自带用户表。第一次做这类系统常犯的错误是一张表塞太多字段导致后续统计和模型特征提取全乱套。合理的设计是“基础信息拆分表 关键维度字段冗余”。首当其冲的是“学生基本信息表”它解决的是“这个人是谁”的问题字段类型说明student_idCharField学号主键nameCharField姓名genderCharField性别majorCharField专业名称collegeCharField学院gradeCharField年级如2024届gpaFloatField平均绩点is_cadreBooleanField是否学生干部intern_countIntegerField实习次数certificate_countIntegerField技能证书数量phoneCharField联系方式等第二张是“就业信息表”它解决的是“毕业去向是什么”的问题。主键用 auto_increment 自增 id用 student_id 做外键关联学生表因为一个学生只有一条就业记录所以加了OneToOneField字段类型说明idAutoField主键studentOneToOneField关联学生表employment_statusCharField就业状态已就业/深造/待就业/灵活就业industryCharField就业行业company_nameCharField签约单位job_cityCharField就业城市monthly_salaryIntegerField月薪元job_positionCharField岗位名称employment_timeDateField毕业去向落定时间另外三张分别是“单位信息表”“问卷反馈表”“用户操作日志表”。单位信息表拆出来的原因是同一家企业会签约多个毕业生如果挂在就业信息表里会产生大量冗余问卷反馈表用于记录离校跟踪回访例如“对母校就业指导满意度”日志表则服务于系统的审计需求。数据库设计的一个核心经验是用“第一直觉”建模往往会拆成比较散的表但真实业务查询里面经常需要联表所以我有限度地做了一些字段冗余比如在就业信息表里直接存了major专业名称虽然违反了第三范式但查询性能和服务端代码复杂度都友好很多。2.2 数据预处理机器学习项目的现实“脏活”原始数据很少是干净的。学校就业系统导出的 Excel 里同一个专业可能有“计算机科学与技术”“计科”“计算机科学”三种写法薪资字段有的是“6000-8000”有的是“6.5k”行业分类这里写“IT/互联网”那里写“信息传输、软件和信息技术服务业”。如果不做预处理机器学习模型和统计报表出来的结果会非常滑稽。我的处理方式是在 Django 项目里单独建一个data_preprocess.py脚本用 pandas 完成三件事。第一步是字段清洗。统一将字符串类型去除首尾空格、统一大小写对“薪资范围”这类字段写函数解析成数值区间的中间值。第二步是类别编码。给“专业”“行业”“城市”这些文本字段做 LabelEncoder 编码或者按需做 One-Hot 编码第三步是缺失值处理。GPA 缺失的用均值填充月薪缺失的按专业中位数填充“就业状态”缺失的直接剔除样本。上代码这是我在项目里实际用过的薪资解析函数import pandas as pd import re def parse_salary(value): 把 6000-8000、6.5k、7000元以上 统一解析为数值 if isinstance(value, (int, float)): return float(value) text str(value).replace(,, ).strip() text text.replace(元以上, ).replace(元, ) if - in text: low, high text.split(-) return (float(low) float(high)) / 2 if text.lower().endswith(k): return float(text[:-1]) * 1000 try: return float(text) except ValueError: return None df pd.read_excel(data/raw_employment.xlsx) df[monthly_salary] df[monthly_salary].apply(parse_salary) df.to_excel(data/clean_employment.xlsx, indexFalse) print(清洗后样本量:, len(df))你可能会问为什么不直接在 Excel 里手工改。说实话几千条数据手工改也能改完但是这是一套要交付给就业办的系统他们要按月更新数据每次更新都手工清理不现实。预处理脚本写好之后新增数据一键跑完这才是系统的价值所在。3. Django 后端与机器学习模块的核心实现细节3.1 后端 API 设计前端要什么我就给什么后端 API 设计不需要过度抽象以“前端页面要展示什么”为导向来定义接口即可。我按页面模块划分了几个核心接口这里列出重要的一部分接口路径方法功能说明/api/auth/login/POST用户登录/api/students/GET分页获取学生列表/api/employment/overview/GET就业率等总览指标/api/analysis/trend/GET历年就业趋势/api/analysis/salary/GET薪资分布与专业对比/api/analysis/industry/GET行业分布统计/api/analysis/cluster/POST调用 K-Means 聚类接口/api/analysis/predict/POST调用薪资预测接口Django REST Framework 的视图集写法非常直白。我举一个就业总览接口的例子这个接口是页面顶部四个核心指标的数据来源# apps/analysis/views.py from rest_framework.views import APIView from rest_framework.response import Response from apps.employment.models import EmploymentInfo from django.db.models import Count class OverviewAPIView(APIView): def get(self, request): total EmploymentInfo.objects.count() employed EmploymentInfo.objects.filter(employment_status已就业).count() pursuing EmploymentInfo.objects.filter(employment_status深造).count() employment_rate round((employed pursuing) / total * 100, 2) if total else 0 avg_salary EmploymentInfo.objects.filter(monthly_salary__isnullFalse) avg_salary_value round(sum(x.monthly_salary for x in avg_salary) / max(avg_salary.count(), 1), 2) return Response({ total_students: total, employment_rate: employment_rate, avg_salary: avg_salary_value, pursuing_rate: round(pursuing / total * 100, 2) if total else 0 })另一个关键设计是“图表数据接口”统一定为{ code: 0, data: {...} }结构前端 Axios 拦截器统一处理错误通过{ code: 500, msg: ... }返回。这样前后端联调时前端只需要关心code 0的情况省掉大量 if-else 判断。3.2 机器学习算法选型与训练K-Means 怎么和业务挂钩这个系统里最有嚼头的部分其实是算法和业务逻辑的对接。我举两个代表性案例讲清楚思路。第一个是 K-Means 聚类。业务需求是“把毕业生就业特征归类成几个典型群体辅助就业办制定分类指导策略”。实现时我取了三类特征月薪、就业城市级别、实习次数然后标准化后聚类。城市级别是自定义映射一线城市3新一线2其他1。跑聚类前一定要做标准化否则月薪几千的数值规模会完全淹没实习次数这类小数值特征。标准化的坑我踩过一次。一开始没标准化聚类结果全部按薪水分层完全看不出群体特征。加上 StandardScaler 之后三个特征权重均衡聚类结果才开始有意义。最终模型将毕业生分成四组系统里我把聚类结果存储到数据库并在前端按聚类分组展示画像。第二个是薪资预测。业务需求是“学生输入自己的简历特征系统预测大概能拿到什么水平的薪资”。训练特征选 GPA、实习次数、证书数量、专业、性别、是否学生干部目标值是清洗后的月薪。我用线性回归和随机森林对比随机森林在测试集上 R² 得分 0.68超过线性回归不少——因为薪资和特征之间明显存在非线性关系。但模型训练不能太频繁我在 Django 里做了一个management command管理员手动触发训练训练好的模型用joblib.dump保存到项目目录models/下。预测时直接加载不走重训练流程。# apps/analysis/ml_service.py import joblib import numpy as np model joblib.load(models/salary_rf.pkl) def predict_salary(features): features 为经过编码和标准化的特征数组 result model.predict(np.array([features])) return round(float(result[0]), 2)第二类值得讲的是“就业影响因素分析”用决策树输出的特征重要性来回答“哪些因素最影响就业”。训练后把model.feature_importances_和特征名组合成 JSON 返回前端画一张横向条形图。用逻辑回归则输出系数并映射成“影响方向”——GPA 系数为正表示绩点越高越容易就业。这类模型不追求极致准确率强调“可解释”和“可汇报”就业办老师拿去写年报时最需要的就是这种结论型输出。3.3 Django 如何优雅地集成机器学习模型机器学习模型在 Django 里的集成关键不在算法而在数据流设计。我的做法是建一个独立的ml_service.py模块把所有模型加载、预测、聚类逻辑封装在内。视图层不直接操作模型对象只调用ml_service的函数。数据流向是这套系统最顺的一环前端点击“开始分析” → Axios 调用后端预测接口 → 视图函数从 POST body 里取出特征数据 → 模型返回原始预测值 → 后端把数值翻译成通俗文案比如“预测月薪 8200 元高于机械类专业均值 12%”→ 前端展示结果。整个过程后端只负责数据处理和业务拼装前端只负责展示和交互职责非常清晰。这里还要注意一个并发问题joblib 模型加载在 Django 开发服务器下没问题但如果用 gunicorn 多 worker 跑生产环境每个 worker 进程会分别加载一次模型。如果你的模型文件几十 MB多 worker 内存占用会翻倍。我后来的处理方式是用global变量做懒加载首次请求时加载一次后续复用避免每次请求都去读磁盘文件。这个坑在生产环境部署时非常典型。4. Vue 前端与可视化大屏的实现过程4.1 前端工程搭建与路由设计前端我基于 Vue CLI 创建项目选配了 Router、Vuex、Axios。依赖安装就一条命令但要提醒一下版本兼容问题。Vue 3 项目建议直接使用 Vite 构建并对 Node 版本有要求16 以上如果坚持用 Vue CLI某些老项目的 Node 8/10 环境会直接报错。安装依赖时建议加上--registry参数使用国内镜像否则下载node-sass、puppeteer这类包经常卡死。路由设计这块我用 Vue Router 4 的普通模式。整体路由表长这样// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, redirect: /dashboard }, { path: /login, component: () import(../views/Login.vue) }, { path: /dashboard, component: () import(../views/Dashboard.vue) }, { path: /students, component: () import(../views/StudentList.vue) }, { path: /analysis, component: () import(../views/Analysis.vue) } ] const router createRouter({ history: createWebHistory(), routes })权限控制用beforeEach路由守卫完成检查本地存储是否包含 token没有就重定向到/login。登录页调/api/auth/login/拿到 token 存到 localStorage。这个方案简单够用生产环境如果要更安全可以改成刷新 token 机制。4.2 ECharts 图表选型不同数据维度用不同图形可视化模块是整个系统最直观的“门面”我基于 ECharts 封装了几个图表组件放在src/components/Charts/下。选图表的逻辑是“先有分析需求再有图形”核心结论优先用最容易读的图形表达。就业总览看板用四个数字卡片总人数、就业率、平均薪资、深造率。行业分布用横向条形图因为行业名称很长纵向条形图的标签会互相遮挡。就业地区流向用地图散点图用城市维度的数据映射到 ECharts 地图上能让“毕业生都去哪儿了”一目了然。薪资分布用直方图或箱线图箱线图在展示不同专业薪资对比时特别直观能同时看出中位数、离散程度和异常值。历年就业率和深造率变化用折线图两条线放一张图里用legend区分。这里给一段“专业薪资对比”的 ECharts 核心配置箱线图是这类系统里比较难写但又最出效果的一个// src/components/Charts/SalaryBox.vue (节选) template div refchartRef styleheight: 360px;/div /template script setup import * as echarts from echarts import { onMounted, ref } from vue const props defineProps({ data: { type: Array, required: true } // [{major, min, q1, median, q3, max}] }) const chartRef ref(null) onMounted(() { const chart echarts.init(chartRef.value) const majors props.data.map(d d.major) const values props.data.map(d [d.min, d.q1, d.median, d.q3, d.max]) chart.setOption({ title: { text: 各专业薪资分布对比 }, tooltip: { trigger: item }, xAxis: { type: category, data: majors }, yAxis: { type: value, name: 月薪(元) }, series: [{ type: boxplot, data: values, itemStyle: { color: #409eff } }] }) }) /script封装组件的好处是 Dashboard 页面只负责传数据图表组件内部处理 ECharts 实例化、销毁、更新。父组件用watch监听数据变化数据一变就调chart.setOption(newData)。不销毁实例直接复用比频繁dispose性能好很多。注意在组件卸载时一定要执行echarts.dispose(chart)否则页面切换多了会出现内存泄漏。4.3 可视化大屏的适配技巧与关键调试就业数据分析系统交付时领导通常要求“投到电脑屏幕上展示”这时候就涉及大屏适配问题。我之前在这块吃亏不少总结出三条实测有效的经验。第一设计稿按 1920x1080 做图表容器用百分比而非固定像素外层div用flex布局。第二用autofit思路做整体缩放把大屏内容包在一个固定比例容器里用 CSS transform 的scale属性按窗口宽度等比缩放。第三EChartsinit时传入的分辨率参数在高分屏下要处理图表模糊问题给canvas加devicePixelRatio有助于提高清晰度。我踩过的另外一个经典坑是在页面刚渲染时容器宽度还是 0此时调用echarts.init图表会变成一个不可见的方块。解决方法是nextTick后再初始化或者window.onresize时调用chart.resize()。这两个点几乎是可视化项目必踩的入门坑。还有地图渲染时首次加载 china 地图数据比较大需要单独引入或通过import异步加载加载完成前不要setOption否则控制台会报“地图数据不存在”的错误。5. 常见问题排查与避坑实录5.1 跨域、请求与开发环境问题速查这个项目最多的问题集中在前后端联调和开发环境。整理了我在开发中遇到过的高频问题和解决方案基本覆盖了新手从头到尾的“求生指南”现象原因解决方案前端请求后端提示 CORS 错误没配置跨域安装 django-cors-headers配置 MIDDLEWARE 和 CORS_ALLOW_ALL_ORIGINSPOST 接口报 403Django 默认 CSRF 校验在 POST 请求携带 CSRF token或对 APIView 使用csrf_exemptGET 能通 POST 不通前端 Axios 没带请求头统一在 Axios 拦截器设置Content-Type: application/json中文乱码MySQL 表编码不是 utf8mb4建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci使用 DevTools 调试时前端修改数据不生效路由复用组件不刷新组件里watch路由参数变化并重新请求数据首次打开大屏图表空白容器宽度未就绪nextTick后echarts.init窗口变化时resizeVue 安装依赖失败npm 镜像问题执行npm config set registry https://registry.npmmirror.com后重装这里面最需要补充说明的是 CSRF 问题。Django 默认启用 CSRF 中间件对所有 POST、PUT、DELETE 请求都做校验。前后端分离场景下前端没有 Django 的模板渲染拿不到csrf_token所以最省事的做法是在登录鉴权相关 API 上加csrf_exempt或者自定义DEFAULT_AUTHENTICATION_CLASSES使用 Token 认证。我项目里使用的是 Django REST Framework 的TokenAuthentication前端登录成功后拿 token后续所有请求在 Header 中带Authorization: Token xxx这样既绕开 CSRF 又实现了权限管理。5.2 机器学习模型集成最容易忽视的细节机器学习的模块单独跑脚本没有问题一旦嵌入到 Django 系统里就容易踩“数据形状不一致”的坑。比如训练模型时对专业做 LabelEncoder编码表存在encoder.pkl里预测接口收到的专业名如果是训练集里从未出现过的值调用transform会直接抛ValueError: y contains previously unseen labels。这种问题的标准解法是“未知类别兜底”。后端接口里捕获异常遇到未见过类别时统一映射为“其他”类或者用encoder.classes_判断一次# apps/analysis/ml_service.py def safe_encode(encoder, value): if value in encoder.classes_: return encoder.transform([value])[0] return len(encoder.classes_) # 兜底类别处理完编码问题还要注意“训练集特征顺序”这个隐性雷区。pandas 的 DataFrame 列顺序会影响模型输入特征顺序训练时是[GPA, intern_count, cert_count, major_encoded]预测接口组装特征时如果顺序变成[major_encoded, GPA, intern_count, cert_count]模型不会报错但结果会完全错乱。我在项目里用的是feature_names列表存特征顺序预测前严格按列表顺序组装避免这种无声的错误。5.3 项目上线与后续扩展的三个建议第一开发完成不要直接在生产服务器上跑python manage.py runserver。我用gunicorn启动 DjangoNginx 反向代理并托管前端打包后的dist静态文件。配置 Nginx 时把/api/路径反向代理到后端端口其他路径指向前端静态文件这一步网上教程很成熟但要注意服务端源码和部署配置纳入版本管理。第二系统后续迭代最值得做的方向是“毕业生就业质量年度报告自动生成”。现在系统已经能出可视化图表再进一步就是把图表数据写入 Word/PDF 模板一键生成图文混合的年度报告文档。Django 生态里有现成的库如python-docx和reportlab我已经在计划里排上了。第三如果数据量继续增大到几十万条建议把统计分析接口改成天然支持大数据的方案Django ORM 走索引聚合查询汇总机器学习训练数据抽到独立分析库后台定期跑离线任务避免分析任务拖垮在线接口。这也是项目从“毕业设计”走向“生产系统”必须迈过的一道坎。做这个项目最大的体会是技术本身不复杂难的是把业务方的模糊需求翻译成系统设计。就业办老师说“我要看学生去哪了”背后其实是“我要知道哪个专业的学生去了哪些城市、行业、薪资多少还要能按年度和学院筛选对比”。你要把这句话拆成清晰的维度和指标才能定义好数据表和接口。另一条经验是数据质量决定了一个数据可视化项目最终能走多远。一个脏数据没处理干净前端再炫的图表也救不回来。如果你正要动手做类似的系统建议先找一份真实的数据集反复清洗这个环节做得越扎实后面的训练、可视化、上线就越顺。最后提醒一句模型不是越多越好能用一张折线图和一张箱线图说清楚的规律不要为了凑模块硬上神经网络。系统能真正回答业务问题才是这个项目的立身之本。