ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django+Vue+机器学习:毕业生就业数据分析系统设计与实现

Django+Vue+机器学习:毕业生就业数据分析系统设计与实现 又是一个毕业设计“top级”选题。Django Vue 这套前后端分离的组合配上机器学习和可视化基本是近两年毕设和项目实战里出场率最高的技术栈之一了。毕业生就业数据分析这个切入点也选得很聪明——数据来源相对明确学校就业系统、招聘平台、问卷调查业务逻辑清晰统计、预测、展示做出来的东西既能看到技术含量又能实际落地到就业指导工作中。我刚完整做完一个同类项目从数据清洗到模型训练再到可视化大屏和部署上线所有坑都踩了一遍。这篇文章就把整个系统的设计与实现过程完整拆解给你从架构设计到每个模块的具体实现包括我实测下来的参数配置和避坑经验照着做你也能复现一套可用性很高的就业数据分析系统。1. 整体架构设计与技术选型思路1.1 核心需求拆解这个系统到底要做什么先别急着敲代码动手之前想清楚一件事——这个系统解决什么问题。如果你只是把数据导进Excel再生成几张图表那不叫系统那叫报告。既然挂上了“系统”的名字就必须具备三个能力数据能进得来、分析能看得懂、结果能查得到。拆解下来毕业生就业数据分析系统的核心需求其实是这四个维度数据管理端管理员录入或批量导入毕业生基本信息、就业去向、薪资水平、企业信息等原始数据。这部分对应Django后台和REST API接口。统计分析端按学院、专业、学历层次、生源地等维度统计就业率、薪资分布、行业流向、地域分布等核心指标。这部分是数据分析模块的主战场。预测研判端基于历史数据训练机器学习模型预测下一年度的就业趋势、薪资水平或者根据学生特征预测其可能的就业方向。这部分是系统的技术亮点也是论文里最能“讲故事”的部分。可视化展示端用图表、大屏、仪表盘等形式直观呈现分析结果让用户教师、辅导员、就业指导中心能快速读懂数据背后的含义。这部分是Vue ECharts的主场。整个系统做成一个闭环管理员维护数据 → Django完成统计和模型预测 → API输出结构化结果 → Vue前端渲染可视化 → 用户看到结论指导决策。1.2 技术栈选型的深层逻辑选型这件事很多人是“因为别人用了所以我用”但答辩的时候老师十个追问里有八个都在问“你为什么选这个”。把选型逻辑想通项目就站得住脚。后端为什么选Django而不是Flask因为这类系统的核心价值不在接口性能而在业务完整性。Django自带的ORM、Admin后台、认证授权、Admin管理界面能帮你在最短时间内搭建一套可维护的后台管理系统。对于就业数据这类结构化程度高、表关系明确的数据Django的ORM写起来比裸SQL舒服十倍。更重要的是Django自带的Admin站点可以直接拿来当数据录入后台毕设演示的时候省一大半开发量。Flask虽然轻量但论坛、认证、权限这些都要自己搭做毕设纯属浪费时间。前端为什么选Vue而不是React中文生态、上手难度、ECharts的融合度这三个维度上都占优势。Vue3的组合式API配合Element Plus组件库写后台管理界面和中屏大屏都很快。更重要的是Vue的单文件组件机制对封装ECharts图表非常友好——一个图表封装成一个组件传入配置项就渲染数据变了自动更新逻辑非常清晰。机器学习部分选什么框架直接用scikit-learn就够了。这个场景下的数据量级通常是几千到几万条SKL的模型完全够用。你不需要上TensorFlow或PyTorch除非你打算用深度学习做一些文本类分析比如就业评价的情感分析但这对毕设来说属于“再无必要”。我实际的选型是随机森林和XGBoost用于薪资预测逻辑回归用于就业方向分类K-Means用于学生聚类分析。这些模型训练快、效果稳定、可解释性也够。可视化部分选ECharts而不是AntV或D3ECharts在中国地图、地图热力、大屏适配上的成熟度和中文文档质量无人能比。就业数据系统里不可避免地要用到“毕业去向地域分布图”——按省份着色ECharts的地图组件一套上去就是满分效果别的库做不到这么省心。1.3 系统架构分层设计把这个系统的架构分成四层来理解数据接入层 → 数据存储与处理层 → 业务逻辑与模型层 → 可视化展示层数据接入层Django Admin后台手动录入、Excel批量导入、CSV导入三种方式。实际做的时候Excel批量导入最重要——学校的就业数据通常以Excel表格的形式提供要支持按模板导入。数据存储与处理层MySQL或SQLite存储业务数据Pandas做数据清洗和转换Django ORM做对象关系映射。这一层的核心任务是“把混乱的源数据变成干净的、可分析的表格”。业务逻辑与模型层统计分析Django ORM聚合 Pandas分组计算、机器学习预测SKL。这一层的输出是JSON格式的统计结果和预测结果。展示层Vue3 ECharts Element Plus通过Axios调用API渲染交互式可视化界面。每一层之间通过API边界分开好处是前端可以并行开发后端改数据结构不会影响前端页面部署时也可以前后端分离部署。2. 数据库设计与Django模型实现2.1 核心表结构设计数据库设计是这类系统的地基。设计得好后面所有查询都能很顺设计得烂那你在ORM里各种join调优的日子会非常难熬。毕业生就业分析系统至少需要这几张核心表学生信息表、就业信息表、企业信息表、专业/学院维度表外加一个用户表用于后台登录和权限控制。我实际使用的模型设计精简后如下# students/models.py from django.db import models class Student(models.Model): 毕业生基本信息表 student_id models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length50) gender models.CharField(性别, max_length10, choices[(male, 男), (female, 女)]) birth_date models.DateField(出生年月, nullTrue, blankTrue) native_place models.CharField(生源地, max_length100, nullTrue, blankTrue) college models.CharField(学院, max_length100) major models.CharField(专业, max_length100) education models.CharField(学历层次, max_length20, choices[(bachelor, 本科), (master, 硕士), (doctor, 博士)]) graduation_year models.IntegerField(毕业年份) is_graduate models.BooleanField(是否应届毕业, defaultTrue) created_time models.DateTimeField(创建时间, auto_now_addTrue) class Meta: db_table student ordering [graduation_year] indexes [ models.Index(fields[major]), models.Index(fields[graduation_year]), ] def __str__(self): return f{self.student_id}-{self.name}class Employment(models.Model): 就业信息表 student models.OneToOneField(Student, on_deletemodels.CASCADE, related_nameemployment) employment_status models.CharField(毕业去向, max_length20, choices[ (company, 企业就业), (civil, 考公务员), (postgrad, 升学深造), (self, 自主创业), (unemployed, 待就业) ]) industry models.CharField(行业领域, max_length50, nullTrue, blankTrue) position models.CharField(工作岗位, max_length100, nullTrue, blankTrue) monthly_salary models.DecimalField(月薪(元), max_digits8, decimal_places2, nullTrue, blankTrue) work_city models.CharField(工作城市, max_length100, nullTrue, blankTrue) company_name models.CharField(签约/就业单位, max_length200, nullTrue, blankTrue) company_type models.CharField(单位性质, max_length30, choices[(国企, 国企), (私企, 私企), (外企, 外企), (事业单位, 事业单位)], nullTrue, blankTrue) is_professional_match models.BooleanField(是否专业对口, nullTrue, blankTrue) confirm_time models.DateField(就业时间, nullTrue, blankTrue) class Meta: db_table employment注意一个细节Employment和Student是一对一关系。一个毕业生对应一条唯一的就业记录这是符合实际的。如果想做“就业过程留痕”功能再单独建一张就业记录流水表但常规统计分析用一对一就够了。2.2 Excel批量导入效率拉满的关键操作手录一万条毕业生数据不现实真实场景下数据都在Excel表里躺着。Django后台单条录入只是一个补充手段批量导入才是核心能力。我的方案是先在Django中做一个Excel导入管理命令然后在视图层提供导入接口。具体步骤如下# students/management/commands/import_students.py import pandas as pd from django.core.management.base import BaseCommand from django.db import transaction from students.models import Student, Employment class Command(BaseCommand): help 从Excel批量导入毕业生数据 def add_arguments(self, parser): parser.add_argument(--file, requiredTrue, helpExcel文件路径) transaction.atomic def handle(self, *args, **options): file_path options[file] df pd.read_excel(file_path, dtype{学号: str}) imported_count 0 for _, row in df.iterrows(): # 字段映射 数据清洗 student_id row[学号].strip() if not student_id: continue if Student.objects.filter(student_idstudent_id).exists(): continue student Student( student_idstudent_id, namerow[姓名], gendermale if row[性别] 男 else female, native_placerow.get(生源地, None), collegerow[学院], majorrow[专业], educationrow.get(学历层次, 本科), graduation_yearint(row[毕业年份]), ) student.save() # 就业信息 if 毕业去向 in row and pd.notna(row.get(毕业去向)): Employment.objects.create( studentstudent, employment_statusrow[毕业去向], industryrow.get(行业, None), positionrow.get(岗位, None), monthly_salaryrow.get(月薪, None), work_cityrow.get(工作城市, None), ) imported_count 1 self.stdout.write(self.style.SUCCESS(f成功导入 {imported_count} 条记录))实际操作中注意三个坑一是学号必须转成字符串。Pandas读取数值型学号默认会带.0我在上面对学号列强制设了dtype{学号: str}但绝杀是末尾还是可能残留全角空格或换行符所以必要时用strip()清理。二是数据校验的兜底。不可能每次都保证Excel模板规范字段缺失、日期格式错乱、学院名称缩写混杂“计科院”vs“计算机学院”都是常见问题。导入逻辑里加一层默认值和异常捕获宁可跳过也不要在中途报错崩掉——批量导入一旦中断恢复成本很高。三是事务保证。用transaction.atomic包住循环要么全部成功要么全部回滚。这避免了导入一半之后发现数据混乱再手动清理几十条脏数据的窘境。2.3 Django REST Framework接口设计前后端分离的话后端必须提供完整的REST API。接口设计直接参考资源语义来# students/urls.py urlpatterns [ path(api/students/, views.StudentListView.as_view()), path(api/students/int:pk/, views.StudentDetailView.as_view()), path(api/employment/, views.EmploymentListView.as_view()), path(api/statistics/overview/, views.OverviewStatisticsView.as_view()), path(api/statistics/trend/, views.TrendStatisticsView.as_view()), path(api/statistics/major/, views.MajorStatisticsView.as_view()), path(api/statistics/industry/, views.IndustryStatisticsView.as_view()), path(api/statistics/geography/, views.GeographyStatisticsView.as_view()), path(api/predict/salary/, views.PredictSalaryView.as_view()), path(api/predict/direction/, views.PredictDirectionView.as_view()), ]在视图层计算统计结果的时候我强烈建议直接写原生聚合查询而不是先把数据全部读到Python里再循环算。Django的aggregate和annotate就是干这个的from django.db.models import Count, Avg, F, Q from rest_framework.views import APIView from rest_framework.response import Response from students.models import Student, Employment class OverviewStatisticsView(APIView): 总览统计数据 def get(self, request): total Student.objects.count() employed Employment.objects.exclude(employment_statusunemployed).count() employment_rate round(employed / total * 100, 2) if total else 0 avg_salary Employment.objects.filter(monthly_salary__isnullFalse).aggregate( avgAvg(monthly_salary) )[avg] or 0 status_distribution Employment.objects.values(employment_status).annotate( cntCount(id) ) return Response({ total: total, employed: employed, employment_rate: employment_rate, avg_salary: round(float(avg_salary), 2), status_distribution: list(status_distribution), })这种写法比遍历对象再找平均值快出一个数量级而且代码可读性高答辩的时候讲起来也清晰。3. 数据分析指标体系设计3.1 核心指标拆解哪些数据真正有分析价值数据量不等于信息量。几千条学生数据导进去之后你需要先想清楚一个问题要告诉用户什么结论我最终把指标收敛成了三组就业总体情况就业率高优先级已就业人数 / 全体毕业生人数。深造率升学研读 / 总数反映学校的学术培养导向。待就业率重点关注指标待就业人数过多说明专业市场供需失衡。薪资与岗位质量平均薪资、最低薪资、最高薪资。薪资中位数比平均薪资更能抵抗异常值影响。专业对口率is_professional_match标记为True的比例。单位性质分布国企/私企/外企/事业单位的比例。行业与地域流向行业TOP10排名计算机软件、制造业、金融、教育等等。流入省市TOP10观察人才虹吸效应。各学院/专业的就业去向跨度。各个指标之间不是孤立的。比如“某专业就业率90%但专业对口率只有40%”——这说明这个专业的学生虽然能找到工作但干的多半与专业无关这反映出专业培养方案和市场需求的错位。这种关联解读正是系统“分析”二字的灵魂也是你论文里可以和指导老师深度探讨的点。3.2 基于Pandas的数据加工流水线Django ORM适合做基础查询但要做多维度分组对比、透视表、时间序列对齐直接用ORM会写到怀疑人生。我的做法是ORM查原始数据 → Pandas转DataFrame → 加工聚合 → 转JSON输出。这套流水线是数据分析类Django项目的通用范式。# analytics/services.py import pandas as pd from django.forms.models import model_to_dict from students.models import Student, Employment def get_salary_by_major(graduation_yearNone): 按专业统计平均薪资 qs Employment.objects.select_related(student).filter( monthly_salary__isnullFalse ) if graduation_year: qs qs.filter(student__graduation_yeargraduation_year) df pd.DataFrame(list(qs.values( monthly_salary, student__major, student__graduation_year ))) if df.empty: return {} result df.groupby([student__graduation_year, student__major]).agg( avg_salary(monthly_salary, mean), median_salary(monthly_salary, median), cnt(monthly_salary, count) ) # 转成前端友好的JSON结构 return result.reset_index().to_dict(orientrecords)你可能会问为什么不在Django里直接玩转ORML即可你有没有搜过随机事09“如果后面数据分析还涉及到多维下钻分析”—“按学院×专业×年份×学历看薪资变化”——ORM filter嵌套到第三层你就要疯Pandas一个groupby([col1, col2, col3])就解决问题。Pandas的长项是灵活分析Django的长项是模型管理两者配合才是数据分析项目的正确姿势。3.3 可视化图表的选型逻辑图表不是越炫越好而是越“切题”越好。拿到ECharts之后我一开始的倾向是这样的——“来一组3D柱状图震一震”。后来发现花哨的图表信息传达效率极低看的人不知道你在表达什么自己也解释不清。最终我定的图表方案是分析维度图表类型为什么选它就业率趋势多年对比折线图时间序列的最优表达变趋势清晰可见各专业就业率对比横向条形图专业名称长横排更易读按数值排序突出高低行业分布占比环形图占比场景最优解中心可放总计地区流向热度中国地图 视觉映射地域维度首选颜色深浅表达热度差异薪资分布频率直方图/箱线图观察集中趋势和离群值箱线图还能看四分位就业方向构成玫瑰饼图升学/考公/创业/企业就业的比例玫瑰图更有冲击力专业就业质量综合对比雷达图就业率、对口率、平均薪资等多个维度投影对比专业强弱把图表选型对应到需求上而不是为图而图系统呈现出来的专业感就完全不一样。4. 机器学习模块的构建与集成4.1 模型定位与选型预测什么才有价值机器学习在这个系统里的角色一定要想清楚——它不是摆着炫技的而是要能回答实际的就业问题。我最终保留了三个预测功能薪资预测回归输入学生特征学历、专业、生源地、性别、是否专业对口等输出预测月薪。这对在校生选专业方向、应届生谈薪资期望都有参考价值。就业方向预测分类输入特征后预测该学生最可能走企业就业 / 考公 / 升学 / 创业 / 待就业。这个对辅导员做精准就业指导很有用。学生特征聚类无监督把毕业生按特征聚成几类比如“高薪技术类”、“稳定体制类”、“深造学术类”、“就业困难类”让管理员从群体层面看清学生构成的底层规律。选型依据很简单数据是结构化表格数据样本量不大几千~几万条特征以类别型字段为主目标诉求是可解释性——scikit-learn里的树模型就是最优解。随机森林能给出特征重要性排序这在答辩环节是加分项XGBoost精度更高逻辑回归可以看每个特征对方向的权重方向适合做归因分析。4.2 特征工程与数据预处理特征决定上限模型只是逼近这个上限。这个项目里特征工程比调参重要得多。从原始数据里衍生出可用的特征我推荐下面这一套方案# ml/features.py import pandas as pd import numpy as np def build_features(): 从原始数据构建模型特征集 df pd.DataFrame(list( Employment.objects.select_related(student).values( monthly_salary, employment_status, student__gender, student__education, student__graduation_year, student__college, student__major, student__native_place, industry, ) )) # 处理缺失薪资——仅保留有薪资标签的样本用于训练回归模型 df_salary df[df[monthly_salary].notna()].copy() # 类别特征编码 df_salary[gender_code] df_salary[student__gender].map({male: 1, female: 0}) df_salary[edu_code] df_salary[student__education].map({bachelor: 1, master: 2, doctor: 3}) # 生源地经济水平分组编码依据省份GDP水平粗略分组 high_gdp [北京, 上海, 广东, 江苏, 浙江, 山东, 福建, 四川] mid_gdp [湖北, 湖南, 河南, 河北, 安徽, 陕西, 重庆, 辽宁, 天津] df_salary[native_economy] df_salary[student__native_place].apply( lambda x: 2 if x in high_gdp else (1 if x in mid_gdp else 0) ) return df_salary注意一个非常关键的细节特征一致性。模型训练时用的特征顺序和字段名必须和预测时接口接收到的数据完全一致。训练时特征顺序是gender_code → edu_code → native_economy → graduation_year预测的时候传参顺序也得一样否则预测结果就是乱码级别的错误。这个看似不起眼的问题是我联调时排查了两天的“大坑”。实操建议把特征处理逻辑封装成同一个函数训练和预测都用它。不要训练时用一套代码、预测时另写一套这是我踩过最深的坑。4.3 模型训练与持久化部署模型训练部分不多说了直接上代码重点是模型训练完之后的持久化和集成方式。# ml/train_salary_model.py import joblib import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score, mean_absolute_error from features import build_features df build_features() # 特征与标签分离 X df[[gender_code, edu_code, native_economy, student__graduation_year]] y df[monthly_salary].astype(float) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model RandomForestRegressor( n_estimators300, max_depth10, min_samples_split5, min_samples_leaf2, random_state42 ) model.fit(X_train, y_train) # 预测测试 y_pred model.predict(X_test) print(fR2: {r2_score(y_test, y_pred):.4f}) print(fMAE: {mean_absolute_error(y_test, y_pred):.2f}元) # 持久化模型 joblib.dump(model, ml_model/salary_model.pkl) # 查看特征重要性 for name, imp in zip(X.columns, model.feature_importances_): print(f{name}: {imp:.4f})训练中要验证“数据拆成回归赛”如果数据量特别少不到2000条建议用回归算法或带正则化的线性回归别硬上复杂树模型否则很容易过拟合。我在用到学校某学院1500多条数据时测试过随机森林的max_depth超过15之后R2反而下降这是典型的过拟合信号。模型训练好之后保存为pkl文件在Django启动时加载一次预测时直接调用# ml/predict.py import joblib from django.conf import settings _model None def get_model(): global _model if _model is None: model_path settings.BASE_DIR / ml_model / salary_model.pkl _model joblib.load(model_path) return _model def predict_salary(gender_code, edu_code, native_economy, graduation_year): model get_model() features [[gender_code, edu_code, native_economy, graduation_year]] return float(model.predict(features)[0])这里有个性能陷阱要提一下模型不要每次请求都从磁盘加载。第一次加载之后放到内存里全局复用不然接口耗时直接从30毫秒干到300毫秒。用全局变量做懒惰加载是我实测下来最简洁可靠的方案。4.4 模型效果验证的几个注意点R2分数不要盲目追求“大于90%”。就业数据本身噪声很大——薪资受到大量未知因素影响行业周期、个人谈判能力、企业规模能达到0.6~0.7就有很好的参考价值了。导师要是问“为什么预测精度不高”你要说“薪资本质上是高噪声变量模型提取的是系统性规律而非个体精确值”这个表述反而是专业的证明。MAE比R2更符合业务逻辑。R2波动对异常值很敏感而MAE直接告诉你“平均误差在2000元以内”。跟非技术背景的就业指导老师解释时说“平均偏差1500元”比“R20.67”更有说服力。模型可解释性要做在前面把feature_importances_的结果做成一个柱状图放在前端“模型解读”页让用户看到“学历贡献最大、占38%专业贡献22%生源地经济水平贡献15%”机器学习模块的落地感完全不一样。5. Vue前端与可视化大屏实现5.1 前端项目结构设计Vue端我用的Vue3 (Composition API) Vite Element Plus ECharts。项目结构按功能模块拆分保证后续扩展不受限frontend/ ├── src/ │ ├── api/ # 接口请求封装 │ │ ├── statistics.js │ │ ├── predict.js │ │ └── student.js │ ├── components/ │ │ ├── charts/ # 图表封装组件 │ │ │ ├── LineChart.vue │ │ │ ├── BarChart.vue │ │ │ ├── PieChart.vue │ │ │ ├── MapChart.vue │ │ │ └── RadarChart.vue │ │ └── layout/ # 布局组件 │ ├── views/ │ │ ├── Dashboard.vue # 数据总览大屏 │ │ ├── SalaryAnalysis.vue # 薪资分析页 │ │ ├── MajorAnalysis.vue # 专业就业分析页 │ │ ├── TrendAnalysis.vue # 趋势分析页 │ │ ├── PredictSalary.vue # 薪资预测页 │ │ └── DataManage.vue # 数据管理页 │ ├── router/index.js │ └── main.js5.2 图表组件封装的关键做法ECharts在Vue里的正确使用方式不是每个页面init来init去而是抽一个通用的图表基础组件所有图表都复用同一个BaseChart通过option参数完成配置。这样表格、折线图、地图都统一了生命周期管理。这里直接给一份我调试好的BaseChart.vue——容器高度不设导致图表不显示这个是ECharts新手十大坑之首!-- components/charts/BaseChart.vue -- template div refchartRef :style{ width: 100%, height: height }/div /template script setup import { ref, onMounted, onBeforeUnmount, watch, nextTick } from vue import * as echarts from echarts const props defineProps({ option: { type: Object, required: true }, height: { type: String, default: 350px } }) const chartRef ref(null) let chartInstance null const renderChart () { if (!chartInstance) { chartInstance echarts.init(chartRef.value) } chartInstance.setOption(props.option, true) // 第二个参数true表示全覆盖 } const resizeChart () { chartInstance chartInstance.resize() } onMounted(() { nextTick(() { renderChart() }) window.addEventListener(resize, resizeChart) }) onBeforeUnmount(() { window.removeEventListener(resize, resizeChart) chartInstance chartInstance.dispose() }) // 数据变化时重新渲染 watch(() props.option, () { renderChart() }, { deep: true }) /script封装之后的用法就是传入不同的option。比如薪资趋势折线图template BaseChart :optionsalaryTrendOption height380px / /template script setup import { computed } from vue import BaseChart from ../components/charts/BaseChart.vue const props defineProps({ salaryData: Array }) const salaryTrendOption computed(() { return { tooltip: { trigger: axis }, legend: { data: [平均薪资, 中位薪资] }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: props.salaryData.map(d ${d.graduation_year}届) }, yAxis: { type: value, name: 月薪(元), axisLabel: { formatter: ¥{value} } }, series: [ { name: 平均薪资, type: line, smooth: true, data: props.salaryData.map(d d.avg_salary) }, { name: 中位薪资, type: line, smooth: true, data: props.salaryData.map(d d.median_salary) } ] } }) /script5.3 大屏布局与数据联动大屏页面是整个系统展示的“门面”也是答辩时最先给人留下印象的部分。我的Dashboard布局方案是经典的三栏式网格布局适应1920x1080的标准屏┌─────────────┬──────────────────┬─────────────┐ │ 就业总览指标 │ 中国地图 │ 就业方向 │ │ (4个KPI卡片) │ (地域流向热度) │ (玫瑰饼图) │ ├─────────────┼──────────────────┼─────────────┤ │ 近五年就业率 │ 专业就业率TOP10 │ 薪资区间分布 │ │ 趋势折线 │ 横向条形图 │ 直方图 │ └─────────────┴──────────────────┴─────────────┘大屏核心不只是放图表联动交互才是关键。我做了两个联动年份筛选器和学院下拉框。用户切换年份所有图表组件会同时刷新点击地图上某个省份右侧饼图同步切换为该省份的就业方向分布。这个交互的代码典型实现是// Dashboard.vue中的联动逻辑 const selectedYear ref() const selectedProvince ref() const loadAllCharts async () { const params { year: selectedYear.value, province: selectedProvince.value } // 并行请求所有图表数据 const [overviewRes, trendRes, geoRes, majorRes, salaryRes] await Promise.all([ fetchOverview(params), fetchTrend(params), fetchGeography(params), fetchMajor(params), fetchSalaryDist(params) ]) // 更新各图表option此处通过响应式数据自动触发子组件重新渲染 overviewData.value overviewRes trendOption.value buildTrendOption(trendRes) salaryDistOption.value buildSalaryDistOption(salaryRes) } // 地图点击事件绑定 const handleMapClick (params) { selectedProvince.value params.name loadAllCharts() }5.4 路由与权限控制前端路由常规配置不做过多讲解重点说一下权限控制。这类系统有一个核心角色划分普通用户和超级管理员。普通用户看数据管理员管数据。我直接用Django DRF自带的Token认证# settings.py REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework.authentication.TokenAuthentication, rest_framework.authentication.SessionAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }前端通过Axios拦截器统一携带Token// api/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Token ${token} } return config }) // 响应拦截统一错误处理 request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { ElMessage.error(登录过期请重新登录) localStorage.removeItem(token) window.location.href /login } else { ElMessage.error(请求失败请稍后再试) } return Promise.reject(error) } )预测结果页面、统计分析页面都是仅登录用户可访问数据管理页面仅管理员可访问。前端路由守卫配合后端鉴权双保险基本满足毕设需求。6. 部署上线与环境配置实战6.1 前后端分离部署方案开发环境跑通之后部署上线时踩的坑往往比开发还多。我最终采用的方案是Nginx Gunicorn Django Node构建Vue静态文件。后端用Gunicorn跑cd /server/project_backend # 安装依赖 pip install -r requirements.txt # 迁移数据库 python manage.py makemigrations python manage.py migrate # 收集静态文件 python manage.py collectstatic --noinput # 用gunicorn启动 gunicorn config.wsgi:application \ --bind 0.0.0.0:8000 \ --workers 4 \ --timeout 120前端构建cd /server/project_frontend npm install npm run build # 构建产物在dist/目录Nginx配置同时承担两件事反向代理API请求到Django托管Vue构建后的静态文件server { listen 80; server_name your_domain.com; # 前端静态文件 root /server/project_frontend/dist; index index.html; # Vue history模式路由重定向 location / { try_files $uri $uri/ /index.html; } # API请求反向代理到Django location /api/ { proxy_pass http://127.0.0.1:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Django Admin后台 location /admin/ { proxy_pass http://127.0.0.1:8000/admin/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }6.2 跨域与接口调试的终极解法前后端分离开发中跨域问题是新人第一次联调必踩的坑。前端跑在8080端口后端跑在8000端口前端请求http://localhost:8000/api/...时必然触发CORS拦截。常规解法是在Django安装django-cors-headerspip install django-cors-headers# settings.py INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # ... ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, http://127.0.0.1:8080, ]但多做一步开发环境的Vite配置代理让前端直接请求/api走Vite代理转发到后端一是避免CORS二是生产环境路径不用改// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 8080, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })这样前端代码里所有请求都用相对路径/api/...开发环境走Vite代理生产环境走Nginx代理代码不用改一行。6.3 部署时的环境配置与依赖锁定部署环境经常出现“我本地能跑服务器不能跑”的情况根因就是依赖没有锁定版本。务必使用虚拟环境并把requirements.txt锁定具体版本pip freeze requirements.txt以我这次项目为例核心依赖版本如下依赖版本说明Django4.2.x稳定版本LTSdjangorestframework3.14.xREST框架django-cors-headers4.3.x跨域解决mysqlclient2.2.xMySQL驱动pandas2.0.x数据处理scikit-learn1.3.x机器学习joblib1.3.x模型持久化gunicorn21.2.x生产服务器还有一个低概率但高杀伤力的坑服务器上MySQL字符集没配utf8mb4中文数据插入直接报错。创建数据库时一定要明确指定字符集CREATE DATABASE employment_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;7. 常见问题与排查技巧7.1 前后端联调高频问题速查这个部分是实打实的踩坑总结我尽量把毕设阶段遇到的高频问题一次性给你梳理清楚问题现象排查方向最终解法前端请求接口报CORS错误后端cors配置安装django-cors-headers在CORS_ALLOWED_ORIGINS加前端地址ECharts图表空白不渲染容器高度图表父级必须有明确高度height: 80vh或固定像素值ECharts图例/表格数据不更新option未触发setOptionBaseChart组件watch option用deep: true深监听加载模型接口报错pkl文件路径不对检查BASE_DIR拼接路径或使用settings.BASE_DIR / ml_modelVue打包后刷新404history路由缺配置Nginx配置try_files $uri $uri/ /index.html中文数据乱码数据库字符集MySQL建库指定utf8mb4Django配置default-character-setutf8mb4批量导入时学号带.0pandas读取类型读取Excel时指定dtype{学号: str}训练和预测结果差很远特征不一致训练和预测统一用同一个特征处理函数7.2 那个折腾我最久的Bug建模预测的接口乱序这个Bug值得单独拎出来讲因为它的教训价值很大。场景薪资预测接口上线之后前端拿到结果怎么测都不对——明明输入“研究生学历、计算机专业、上海生源”预测薪资比很多本科生还低完全违背常识。排查过程先怀疑模型本身回训练集测试R2 0.72测试集预测效果正常。排除模型问题。再怀疑数据传参错误前端console打印请求payload特征值没问题。排除入参问题。最后直接在后端打印进入model.predict()之前的特征矩阵一眼发现问题——特征列顺序错位了。问题在于Django接口代码里重新写了特征构造逻辑字段顺序和训练时的不一致训练时是[gender, edu, native_economy, year]预测接口里写成了[gender, year, edu, native_economy]。模型接收到的东西完全变了意思预测结果自然荒谬。经验教训特征工程的代码在训练和预测中必须共享同一个函数、同一个顺序。永远不要在两处各写一套。这个教训价值远超这个Bug本身——团队协作或者后续维护时改动一处的逻辑另一处不知道就会出这种“看起来没坏但结果全错”的隐形Bug。也正是通过这个教训我更确信一个结论数据类项目里出错往往是错在流程一致性而不是算法本身。8. 写在最后项目交付的实际心得整个项目从开发到上线一个多月的时间主要集中在三个方面数据清洗真的占了40%的工作量、模型调优和业务解释花了30%、剩余的才是前后端联调和部署。如果你也打算复现类似项目把这60%的时间预留出来否则后面节奏会非常赶。关于毕业论文/报告里怎么写一个核心建议不要只写“我实现了技术栈ABC”而要从业务问题切入——学校就业指导面临什么难点、现有方案有什么不足、系统如何用数据驱动决策——把逻辑链条写清楚导师的阅读体验会好很多。最后分享一个小技巧也是深有体会的一点模型预测部分做一个人性化的输入界面会很加分。我做了个“我的薪资预测”页面用户选几个选项就能看到预测月薪区间和影响因素贡献占比用实际可感知的交互把“机器学习”这个抽象名词转成了每个人都能体验的功能。这种将技术转化为用户可感知价值的设计远比堆叠技术名词更能打动答辩老师。如果你照着这套架构和实现方案走一遍你得到的不只是能跑通的一套系统更是一个逻辑自洽、有深度也有落地感的完整项目。
返回列表