☰
Django+Vue.js考研分数线预测与院校推荐系统毕设全解析
2026/10/16 11:19:10 网站建设 项目流程

带过几届毕业设计之后,我越来越觉得,计算机类的毕设选题不能只看"技术新不新",更要看它能不能讲清楚一个问题、能不能完整走通一条数据链路。今天想拆的这个题目——Django+Vue.js考研分数线预测系统、考研院校推荐系统,就是很典型的一个:技术栈成熟、业务场景真实、数据链路完整,而且不管是做预测还是做推荐,都有明确的用户痛点和可验证的结果输出。换句话说,它像一个"麻雀虽小五脏俱全"的项目,非常适合作为大数据方向的毕业设计。

这篇博文我会从需求拆解、架构设计、数据库建模、预测模型落地、推荐算法实现、前端页面开发,到最后的常见坑点排查,完整走一遍。目标读者是那些选了类似题目但还不清楚从哪下手、或者想把这个项目做扎实的同学。我会尽量把每个环节里"当时我也纠结过"的决策过程写出来,而不是只给结论。

1. 项目拆解与核心需求解析

1.1 分数线预测:从一个很现实的焦虑说起

考研择校这件事,信息不对称是最大的问题。很多同学在三四月份决定要考某校某专业时,手头能参考的往往只是去年的录取分数线、一两个经验帖、以及学长学姐的描述。但分数线本身受报名人数、试卷难度、招生计划变化、当年国家线波动等因素影响,只看单一年份的数据几乎等于猜。

所以这个系统的第一个核心需求就是分数线预测。预测对象分三层:国家线、院校线、专业复试线。国家线是全国统一的基准线,比较容易预测;院校线是各校根据报考情况划的线,波动更大;专业复试线则是真正决定你能不能进复试的那条线,也最有价值。

预测的本质是一个回归问题——基于历史年份的分数序列,以及影响分数的外部特征,预测未来一年的分数线区间。注意我说的是"区间",不是"一个精确值"。因为分数线受当年试卷难度这种不可控因素影响很大,精确到个位数的预测既做不到,也不该承诺。好的毕业设计应该有这个分寸感,把预测结果以区间或概率的形式呈现,并且给出置信度的概念。

1.2 院校推荐:不只是"分数够不够"

有了预测分数之后,用户下一步自然要问:我这个条件,能冲哪些学校、稳哪些学校、保哪些学校?这就是院校推荐系统的需求来源。

但推荐不能只看预测分数线。一个完整的推荐决策涉及的因素至少包括:本科院校层次(双一流/普通一本/二本,涉及复试歧视风险)、目标地区(东部中部西部,涉及判卷宽松度和就业地缘)、专业门类(工科、理科、经管、文科的分数结构差异很大)、学硕还是专硕、英语和政治难度预期、是否接受调剂,以及最重要的——近几年报考热度和报录比。

所以我把推荐系统定位成"规则过滤 + 多维评分排序"两段式结构。先通过硬性规则把完全不可能的和用户明确排除的院校过滤掉,再对剩下的院校做综合评分,按"冲、稳、保"三档输出。这个设计比单纯做一个"分数填报查询"要更有说服力,答辩时也更容易讲清楚。

1.3 技术选型:为什么是Django+Vue.js这套组合

先说结论:这个组合非常适合做毕业设计,而且是那种"不会翻车"的选择。

Django的优势在于:自带ORM、Admin后台、用户认证体系,开发效率极高。尤其Admin后台,录入历年分数线数据、维护院校信息,直接就能用,省掉大量后台管理页面的开发时间。另一个关键点是——Python生态。分数线预测要写特征工程、模型训练,用Python做这件事再顺手不过了。训练好的模型用joblib或pickle存下来,Django view里直接加载调用,技术链路非常顺畅。这一点是SpringBoot那套Java技术栈很难比的。

前端选Vue.js则看中的是组件化开发和数据可视化生态。ECharts配Vue 3,画近几年分数线趋势图、报录比柱状图、分数分布热力图,都是现成的组件。再加上Element Plus提供表单、表格、卡片、对话框等一整套UI组件,你不需要自己从零写样式,能把精力放在业务逻辑上。

如果之前只接触过PHP或者纯模板渲染的开发方式,这套前后端分离的架构可能需要一点时间适应,但一旦理解了"后端只管数据接口、前端只管页面交互"的边界划分,开发协调上反而更清晰——前端同学跟后端同学各改各的,联调时对齐接口就行。单人完成毕设时,这种边界也让代码结构更可控、更不容易写到一半乱七八糟。

2. 系统架构与数据库建模

2.1 前后端分离的目录结构设计

整个项目的目录我建议分成前后端两个独立工程,不要混在一个目录里。这样部署时思路清晰,答辩讲架构时也更容易说清楚。

GraduateAdmissionSystem/ ├── backend/ # Django工程 │ ├── manage.py │ ├── requirements.txt │ ├── configs/ # 全局配置 │ │ ├── settings.py │ │ ├── urls.py │ │ └── ... │ ├── apps/ │ │ ├── users/ # 用户模块 │ │ ├── schools/ # 院校与分数线模块 │ │ ├── predict/ # 预测模块 │ │ └── recommend/ # 推荐模块 │ ├── ml_models/ # 模型文件与训练脚本 │ │ ├── train_score_model.py │ │ ├── model_store/ │ │ └── feature_utils.py │ └── scripts/ # 数据导入清洗脚本 ├── frontend/ # Vue3工程 │ ├── package.json │ ├── vite.config.js │ ├── src/ │ │ ├── api/ # axios封装 │ │ ├── views/ # 页面组件 │ │ ├── router/ │ │ └── components/ └── documents/ # 说明书、PPT、SQL脚本

这里有个细节:把训练脚本和模型存放目录独立出来,不要跟Django的业务代码混在一起。因为模型不是每次请求都要重新训练,它应该在离线环境训练好、保存成文件,在线环境只负责加载和预测。这个"离线训练、在线预测"的架构,是机器学习工程里非常基础也非常重要的思想,写进文档里会是个加分项。

接口风格用RESTful。典型接口包括:

  • POST /api/users/register注册、POST /api/users/login登录
  • GET /api/schools院校列表(支持校区、地区、类型筛选)
  • GET /api/schools/{id}/score_lines某院校历年分数线
  • POST /api/predict/score提交特征,返回预测分数区间
  • POST /api/recommend/schools提交用户画像,返回推荐院校列表
  • GET /api/recommend/options获取筛选条件的枚举值

Django端需要处理跨域,直接在settings里装django-cors-headers:

# settings.py INSTALLED_APPS = [ # ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # ... ] CORS_ALLOWED_ORIGINS = [ 'http://localhost:5173', # Vite开发服务器 'http://127.0.0.1:5173', ]

注意中间件顺序,CorsMiddleware最好放在其他中间件之前,否则某些情况下响应头会加不上。我早期在这个地方踩过坑,接口数据都正常返回了,但前端就是读不到,排查半天最后发现是中间件顺序问题。

2.2 核心数据表设计

数据库我用MySQL,原因很朴素:毕设答辩环境跑这种系统,MySQL比PostgreSQL更常见,同学自己在本机装也方便。下面这几张表是核心中的核心,设计时用点心能省后面很多事。

第一张是院校信息表。字段除了基础名称、地区、院校层次,我建议预留"院校特色标签"字段,比如理工类、师范类、财经类、综合类,这是推荐系统做画像匹配时的重要维度。

# apps/schools/models.py from django.db import models class School(models.Model): """ 院校基础信息表 """ name = models.CharField('院校名称', max_length=128, unique=True) province = models.CharField('所在省份', max_length=32) # 冗余省份名称 city = models.CharField('所在城市', max_length=32) region = models.CharField('所属大区', max_length=16) # 华东/华北/华南/西部等 level = models.CharField('院校层次', max_length=32) # 双一流A/B、普通一本、普通二本 category = models.CharField('院校类型', max_length=32) # 理工/师范/财经/综合等 is_985 = models.BooleanField('是否985院校', default=False) # 避免直接用"985"作为枚举值,用布尔位便于筛选 is_211 = models.BooleanField('是否211院校', default=False) is_double_first_class = models.BooleanField('是否双一流', default=False) num_graduates = models.IntegerField('每年毕业人数', null=True, blank=True) tags = models.CharField('院校特色标签', max_length=255, blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) class Meta: db_table = 'school' ordering = ['id'] def __str__(self): return self.name

生产环境当然不推荐用is_985这种布尔字段去描述五位一体式的复杂层次结构,那应该用关联表或者多值枚举。但对于毕设的体量,布尔位直接、好查、好展示,我认为是合理的取舍。答辩时如果能主动说出"这里我做了简化取舍,如果未来扩展可以改成标尺表",反而显得你思考过。

第二张是专业信息表。注意专业不能只存一个名称,因为不同院校的同一专业名称可能不同,但学科门类是一致的。

class Major(models.Model): """ 专业信息表 """ code = models.CharField('专业代码', max_length=16, unique=True) name = models.CharField('专业名称', max_length=64) category = models.CharField('学科门类', max_length=32) # 哲学/经济学/法学/教育学/文学/历史学/理学/工学/农学/医学/管理学/艺术学 is_master_degree = models.BooleanField('是否学术型硕士点', default=True) is_doctor_degree = models.BooleanField('是否博士点', default=False) description = models.TextField('专业描述', blank=True) class Meta: db_table = 'major'

第三张是历年分数线表,这是整个预测功能的数据底座。字段设计上有一条经验值得记住:把"分数线的科目构成"拆开存。考研分数线不是单一数字,它包含总分线和各科单科线。如果只存一个总分,预测模型的输入维度就少了一大块;更关键的是,某些专业总分涨了但单科线降了,这种结构性变化只有拆开存才能捕捉到。

class ScoreLine(models.Model): """ 某院校某专业某年分数线记录 """ school = models.ForeignKey(School, on_delete=models.CASCADE, related_name='score_lines') major = models.ForeignKey(Major, on_delete=models.CASCADE, related_name='score_lines') year = models.IntegerField('年份') category = models.CharField('硕士类型', max_length=16) # 学硕/专硕 total_score = models.IntegerField('总分线') politics_score = models.IntegerField('政治单科线') foreign_language_score = models.IntegerField('外语单科线') business_course_one_score = models.IntegerField('业务课一单科线') business_course_two_score = models.IntegerField('业务课二单科线') enroll_count = models.IntegerField('招生人数', null=True, blank=True) applicants_count = models.IntegerField('报考人数', null=True, blank=True) admission_rate = models.FloatField('报录比', null=True, blank=True) source = models.CharField('数据来源', max_length=255, blank=True) class Meta: db_table = 'score_line' unique_together = ('school', 'major', 'year', 'category')

说几个建模时的关键判断。冗余地区和大区字段:虽然可以通过省份坐标换算,但接口筛选时频繁做坐标换算会让查询变复杂,冗余存储直接查就行。报录比字段允许为空,因为早年数据确实采集不到,为空的记录预测时要单独处理。联合唯一约束(school, major, year, category)保证了同一条数据不会重复导入,这个约束在数据清洗脚本里是兜底防线。

用户相关表我反而建议做得简单一点:用户表存账号密码和基本画像(目标专业门类、意向地区、本科层次),再加一张收藏表存用户收藏的院校。很多同学在用户模块上花太多时间做积分、做关注、做评论,这些功能对毕设核心卖点的提升很有限,不如把时间留给预测模型和推荐策略。

2.3 字段设计里的经验教训

我见过很多半途而废的毕业设计,数据库设计缺陷是主要翻车原因之一。最常见的问题有两个。

第一个是把年份相关的字段设计成属性列。有些同学图省事,设计成score_2023、score_2024,结果后续要加年份时就得改表结构,写指标查询时也要写一堆动态列拼接的脏代码。正确做法就是上面的行式存储,一条记录对应"某校+某专业+某年+某类型",这样不管是预测训练集的构造(按年排序)还是前端画趋势图(按年遍历),都是最自然的方式。

第二个问题是不区分数据属性。例如,把"是否接受调剂"这种每年都会变化的政策信息,设计成一个固定枚举,这会导致年份变化后推荐结果失真。没错,学费、宿舍条件、导师资源这些每年都可能变。分数线表和院校表里的数据时效性完全不同:院校的基础属性(层次、类型)几年不变,而分数线和招生计划每年都在变。建模时要把稳定性信息和时效性信息分表存放,查询时关联即可。

3. 分数线预测模型落地

3.1 数据准备与合规处理

预测模型的成败,六成在数据,三成在特征,模型算法本身只占一成。对毕设项目来说,你需要先把数据攒起来。

分数线数据获取途径,我建议分两步。第一步,用爬虫去研究生招生信息网和各院校官网的历年公示页面爬公开数据。这里必须强调:只爬公开的、可合法访问的数据,控制并发频率,不要爬任何需要登录或者明确禁止采集的信息。某高校公示的复试名单和拟录取名单,包含分数、专业、初试复试成绩,是很好的数据来源。第二步,公开数据覆盖不足的部分,用人工录入补全,Django Admin直接可以操作。

数据清洗这一步很多人会跳过,但预测结果不准往往就是栽在这。常见的脏数据有:年份字段格式不统一(有的年份是四位数字、有的带"年"字);专业名称不一致("计算机技术"和"计算机科学与技术"被当成两个专业);同一学校临时改了校名导致的历史数据断层;某些年份分数线缺失但前后年份都有值。

清洗策略分两类。缺失值:如果某缺失年份前后两年都有数据,用前后均值填充;如果连续缺失三年以上,建议直接丢弃该序列,因为填充的可靠性太低。异常值:如果某年分数线突然比前后两年高出三四十分,先核实原始数据,确认无误后保留——这种异常往往是报考人数暴增造成的,对预测模型来说反而是有信息量的样本,不应该当脏数据处理。

3.2 特征工程:分数线到底被什么影响

建模之前得先想明白一个问题:分数线是果,它的因是什么?我整理过一轮,把特征分成三组。

第一组是历史趋势特征。前一年的总分线、前两年的总分线、近五年分数线的均值和方差。这组特征捕捉的是分数线的惯性——通常分数线不会一年内暴涨暴跌,连续变化比例在20%以内是常态。

第二组是报考热度特征。当年的报名人数、招生人数、报录比,以及这三者与前三年的比值。注意,报名人数和招生人数的"增长率"往往比绝对值更有预测力。比如某专业招生从50人缩到30人,即使报考人数不变,分数线也大概率上移。

第三组是外部环境特征。当年的国家线总分、国家线单科线、当年公共课平均难度(可以用样本考生平均分近似)、目标院校所处地区的公共课判卷宽松系数。这个判卷宽松系数是个很微妙的东西——同样是英语二,某省份平均分可能比另一省份高七八分,这会直接影响总分线的实际含义。网上一直有考生把它叫作"水区旱区",虽然是非官方说法,但作为特征输入模型是合理的。

特征并不是越多越好。我最初做过一个版本,把四十多个特征一股脑塞进去,结果模型在训练集上表现很好,测试集上完全失控——典型的过拟合。后来砍到十一个特征,用特征重要性排序保证每个特征都有独立信息量,效果反而稳定了。

3.3 模型选型与训练脚本

想在毕业设计里用LSTM做时序预测,这个念头多半是看了某篇论文。但我的建议是:先用简单模型跑通,再按需升级。

简单模型首选XGBoost(或者随机森林),理由是:

  • 考研分数线数据量级就几百到几千条样本,深度学习模型在这种小数据量上很难发挥优势,反而容易过拟合。
  • 梯度提升树对特征的单位和分布不敏感,不需要做复杂的归一化,工程上省事。
  • 模型可解释性强,能输出特征重要性,答辩时可以清晰讲出"影响分数线最大的因素是往年分数和招生人数变化",这是一个非常有力的展示点。

我当时做的第一版基线模型反而是线性回归,用来做"下限对比"——即验证"一个极其简单的模型能达到什么效果"。答辩时能把"简单模型 vs 集成模型"的对比数据放出来,比单纯吹自己模型多精确更有说服力。

训练脚本独立放在ml_models目录下:

# ml_models/train_score_model.py import pandas as pd import joblib from sklearn.ensemble import GradientBoostingRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error # 1. 从数据库/CSV读取特征样本 data = pd.read_csv('score_dataset.csv') # 关键点:去掉泄漏信息的时间字段,年份不能作为回归目标 feature_cols = [ 'last_year_total_score', 'avg_score_5y', 'std_score_5y', 'applicants_growth_ratio', 'enroll_change_ratio', 'national_line_total', 'national_line_single', 'exam_difficulty_ratio', 'region_mark_coefficient', 'school_level_code', 'is_master_degree', ] X = data[feature_cols] y = data['total_score'] # 2. 划分训练集与测试集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.15, random_state=42 ) # 3. 训练回归模型 model = GradientBoostingRegressor( n_estimators=200, max_depth=4, learning_rate=0.08, random_state=42, ) model.fit(X_train, y_train) # 4. 评估 y_pred = model.predict(X_test) print('MAE: %.2f' % mean_absolute_error(y_test, y_pred)) # 5. 保存模型文件 joblib.dump(model, 'model_store/score_model_xgb.joblib') # 6. 输出特征重要性 importance = sorted( zip(feature_cols, model.feature_importances_), key=lambda x: x[1], reverse=True, ) for name, value in importance: print('%s: %.4f' % (name, value))

一个非常重要的细节:切分训练集和测试集时不能随机切分,而要按时间顺序切分。比如用2018到2022的数据训练,2023和2024的数据做测试。为什么?因为随机切分会让模型"偷看"到未来数据——某一年份的样本既出现在训练集又可能出现在测试集的同一年,在真实预测场景中这是不可能发生的,测试结果会虚高。这叫时间序列数据的时间切分原则,很多初学者在这里栽了跟头还误以为模型很厉害。

模型评估用平均绝对误差(MAE)而不是准确率。分数线的预测误差在5到10分以内,说明模型已经具备实用参考价值。你想,考研复试线很多专业分分相差能达几十分,"预测误差在10分以内"意味着能把用户的目标院校锁定在一个相对靠谱的范围内。

3.4 预测接口的封装

模型训练好以后,Django端要做的工作就是:接收前端传来的特征,加载模型文件,输出预测区间。

# apps/predict/views.py import joblib import numpy as np from django.conf import settings from rest_framework.views import APIView from rest_framework.response import Response model = joblib.load(settings.BASE_DIR / 'ml_models/model_store/score_model_xgb.joblib') class ScorePredictView(APIView): def post(self, request): # 前端传来的特征 feature_dict = request.data # 构造特征向量,顺序必须与训练时一致 feature_cols = [ 'last_year_total_score', 'avg_score_5y', 'std_score_5y', 'applicants_growth_ratio', 'enroll_change_ratio', 'national_line_total', 'national_line_single', 'exam_difficulty_ratio', 'region_mark_coefficient', 'school_level_code', 'is_master_degree', ] X = np.array([[feature_dict.get(col, 0) for col in feature_cols]]) pred = model.predict(X)[0] # 输出区间:点预测值 ± 模型误差的1.5倍 lower = max(0, int(pred - 8)) upper = int(pred + 8) return Response({ 'predicted_score': round(pred, 1), 'interval': [lower, upper], 'level': '高风险' if feature_dict.get('school_level_code', 0) >= 2 else '中等风险' })

模型文件每学期重新训练一次就够了,不需要在线更新,所以启动时加载一次到内存完全没问题。我这里输出的"高风险/中等风险"是一个简化的风险标签,实际生产会用分位数来算概率,但毕设做到这个程度已经完全能展示。

4. 院校推荐系统实现

4.1 推荐流程设计

推荐系统不需要上协同过滤、矩阵分解这种高级算法。原因很简单:这个场景没有"用户行为数据"——用户最多收藏几所学校,没有足够的点击、浏览、报考行为来支撑协同过滤。强行套用协同过滤是典型的为了用算法而用算法。

实际可行的方案是内容过滤 + 规则打分。先构建用户画像,再计算用户画像与每个院校特征的匹配度。匹配度由四个维度的得分加权求和得到:

  • 分数匹配度(权重0.4):用户预估分数 / 目标院校近三年平均复试线。比值在1.1以上算稳,1.0到1.1算适中,0.9到1.0算需要冲刺。
  • 层次匹配度(权重0.25):用户本科层次与目标院校层次的关系。双一流冲击双一流加满分,普通二本冲击顶尖院校加分低甚至为零,避免推荐完全可望不可即的学校。
  • 地区偏好匹配度(权重0.2):用户意向地区命中加分,同时考虑院校所在地区的就业承载力。
  • 专业热度匹配度(权重0.15):目标专业的报录比热度与用户风险承受能力是否匹配。用户选择"求稳"时,高热度专业降权。

这个权重不是拍脑袋定的,我做了一个简单的问卷:把十组不同的推荐结果给同届考研的学生选,统计他们更认可哪种排序方式,用排名数据反推权重。虽然样本量只有几十人,但它至少说明权重是"从用户反馈中来的",而不是我对着空气编的。

排序完成后再打档位:综合得分前20%标为"冲刺院校",中间50%标为"稳妥院校",后30%标为"保底院校"。同时,每一个推荐的院校卡片都要给出"推荐理由"的文本解释,例如:"该校近三年复试线稳定在355至365分,与你预估分数匹配度较高,且该专业历年招生人数稳定在22人左右,风险较低。"在毕业设计答辩里,可解释的推荐理由比黑盒排序结果更能打动人。

4.2 过滤规则与边界情况

过滤阶段要处理几个边界情况。

第一,用户预估分数为空的冷启动问题。允许用户不做分数预测、直接按地区+专业浏览推荐结果。此时评分模型跳过分数匹配度维度,其余三个维度按比例重新分配权重。这个设计让系统不至于因为没有预测结果就整个不可用。

第二,不接受调剂的用户要过滤掉往年显示有大量调剂名额的院校。反过来,接受调剂的用户,可以单独给一个"调剂友好院校"列表,这个列表的数据来源是往年拟录取名单里"调剂"标注的记录比例。

第三,地区偏好的粒度。如果用户只选了"浙江",但该省院校资源少,系统会自动扩展到同一大区的院校,并提示"浙江地区院校较少,已为你扩展至华东区域推荐"。这种扩展策略处理了数据稀疏问题,也有实际意义——考研本来就有很强的跨省流动性,没必要把范围锁死在一个省。

4.3 推荐接口的代码实现

推荐接口的伪代码逻辑可以这样组织:

# apps/recommend/services.py def recommend_schools(user_profile): # 1. 硬性条件过滤 schools = School.objects.filter( province__in=user_profile.provinces, major__category=user_profile.major_category, ) # 2. 软性条件过滤(可配置) if not user_profile.accept_adjust: schools = [s for s in schools if s.adjustment_ratio < 0.2] # 3. 综合评分 scored = [] for school in schools: score = ( 0.4 * match_score(user_profile.predicted_score, school.avg_score) + 0.25 * level_match(user_profile.bachelor_level, school.level) + 0.2 * region_match(user_profile.target_region, school.region) + 0.15 * heat_score(user_profile.risk_attitude, school.applicants_growth) ) scored.append((school, score)) scored.sort(key=lambda x: x[1], reverse=True) # 4. 切三档 total = len(scored) return { 'rush': scored[:int(total * 0.2)], 'steady': scored[int(total * 0.2):int(total * 0.7)], 'safe': scored[int(total * 0.7):], }

这段代码里有个容易忽略的细节:school.avg_score不是院校总分线的简单平均,而是该院校在用户目标专业上近三年的平均复试线。因为同一个学校不同专业的分数线可以相差五六十分,用全校统一平均分去匹配没有意义。所以这个字段的查询要关联到专业维度,而不是简单的School模型字段。

还有一个建议:评分过程放在Django service层而不是view层,view层只做参数校验和Response包装。这点在做代码审查时会很加分——职责单一,view没有业务逻辑。

4.4 可视化分析的可加分方向

推荐系统做完基础功能以后,我建议加一个"考研大数据分析"页面,这是大数据毕设的题目内涵所在。不用做得很复杂,三个表就够了:

  • 近十年国家线趋势折线图,按学科门类分色展示。这个图能让用户一眼看出自己专业的分数趋势是上行还是平稳。
  • 各省份学子报考热力地图,用省份报考人次数据着色的中国地图。数据量不大,但视觉冲击感强。
  • 院校分数线变动Top 10排行榜,列出近三年涨幅最大的院校和专业。

ECharts做这些图也就十几行配置,但对"大数据毕业设计"的包装帮助很大。答辩时,页面上有图、有数、有结论,比纯表格要直观得多。

5. Vue.js前端核心实现

5.1 项目初始化与路由设计

前端用Vite创建Vue 3工程,比Vue CLI快很多。创建时选择vue-router和pinia作为依赖,UI组件库单独安装Element Plus。

npm create vite@latest frontend -- --template vue cd frontend npm install element-plus axios vue-router@4 pinia echarts

路由设计上,我希望页面结构清晰不冗余:

  • /首页:项目简介 + 快捷入口
  • /predict分数线预测页:输入特征,展示预测结果和趋势图
  • /recommend院校推荐页:填写画像,展示冲稳保卡片列表
  • /schools院校库:筛选浏览院校与分数线明细
  • /analysis数据分析页:几个可视化大图
  • /login/register登录注册页

其中/recommend和/analysis是核心页面,演示时打开就是完整功能,不用绕路,这个细节在导师演示当天很重要——如果还要先登录、再点好几个菜单才能看到核心页面,体验就很差了。

5.2 预测页面的交互与可视化

预测页的表单字段不多,大概七个:大学代号(用于查院校层级)、目标专业门类、预估公共课水平、目标地区、意向硕士类型(学硕/专硕)、风险偏好、是否接受调剂。提交后调用后端的/api/predict/score接口,拿到预测区间,然后在ECharts上把近五年历史分数线折线和预测区间带叠加展示。

// src/api/predict.js import request from '../utils/request' export function predictScore(data) { return request({ url: '/api/predict/score', method: 'post', data, }) }

预测结果区域我建议放三个东西:大号的预测分数数字、预测区间区间带(用半透明色带表示浮动区间)、和一句话解释("你的预估分数与该专业近三年平均线基本持平,处于中等风险区间")。这个"解释"后端要返回,前端直接展示即可。人话解释是产品感和技术感最直接的差异。

画趋势图时有一个小坑要注意:ECharts横轴如果是年份,数据量少(只有五六年),默认的类目轴是均匀分布的,这是对的。不要因为看到X轴年份间隔不均匀就试图改成时间轴,那会导致年份间隔被拉伸成等距,反而不符合真实时间逻辑。

5.3 推荐页面的卡片设计与筛选逻辑

推荐页是系统里最复杂的页面,因为它的交互链路长:选择筛选条件 -> 提交 -> 返回三组卡片 -> 点击卡片查看详情 -> 详情里展示理由和分数线明细。

筛选条件用Element Plus的el-form布局,条件包括:目标省份(多选)、专业门类、预估分数或预测结果、学硕专硕、风险偏好、是否接受调剂。提交按钮调用/api/recommend/schools接口。

结果区用三块Tab或三个分组展示"冲刺、稳妥、保底"。每个院校卡片包含:

  • 校名和院校层次标签(Element Plus的el-tag)
  • 近三年复试平均分和报录比
  • 预估分数与平均线的差值(用"+8分"或"-5分"这种直观表达)
  • 推荐理由(后端生成的文本)

卡片底部放两个操作按钮:"查看详情"打开分数线明细抽屉,"收藏"调收藏接口。收藏功能虽然是小功能,却能让演示时有一个连贯的操作流:预测 -> 推荐 -> 收藏 -> 在个人中心看到收藏列表。

5.4 前端工程化细节

接口地址统一放src/utils/request.js里封装axios,设置baseURL为/api,这样本地开发时配置Vite代理到Django端口:

// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, }, }, }, })

用代理而不是在前端写死http://127.0.0.1:8000,最关键的好处是之后部署到生产环境时不用改前端代码——Nginx统一反向代理后,前端请求的同源地址自动转发到Django服务。这个习惯如果从一开始就养成,部署debug的时间可以省出一整天。

打包构建时还要注意history路由的问题。Vue Router如果使用createWebHistory,生产环境必须让服务器把所有非静态文件请求都转发到index.html,否则在/recommend直接刷新页面会404。Nginx配置里加一行try_files $uri $uri/ /index.html;就能解决。如果你不想折腾服务器重写规则,最简单的方案是改用createWebHashHistory,URL里带个#号,但美观性差一点。我建议毕设直接用history模式加Nginx一行重写规则,讲解部署时还能顺便聊两句。

6. 常见问题与排查指南

6.1 十个高频坑点实录

这批问题是我在这个项目类型的开发过程中真实遇到过、也帮学生排查过的,按出现频率排序做成一个速查表。

现象根本原因解决办法
前端请求后端报CORS错误忘记配置django-cors-headers或中间件顺序不对检查settings.py,把CorsMiddleware放到中间件最前
axios请求返回403CSRF token校验失败前后端分离后用JWT认证,或对API视图标注csrf_exempt,用JWT替代session
模型预测结果始终是同一数值前端传来的特征字段名和训练时不一致,取默认值0在接口返回调试字段,打印实际接收的特征字典
训练集R²很高但测试集一塌糊涂随机切分了时间序列数据改成按年份切分,保证训练集年份全部早于测试集
推荐接口报"列表为空"过滤条件过严,比如省份只选了一个且没有匹配数据增加扩展策略:同大区扩展并提示;同时过滤条件前端做非空校验
前端打包后刷新404history路由没有配置重写规则Nginx加try_files规则,或改用hash模式
ECharts图表显示空白容器div高度为0,或图表初始化在异步数据返回前给容器设置固定高度,在nextTick里初始化图表
Element Plus组件样式不对全量引入了Element Plus但没正确注册确保app.use(ElementPlus)且引入样式element-plus/dist/index.css
登录后刷新页面就掉登录态JWT存在localStorage但没有在请求拦截器里带tokenaxios拦截器里从localStorage取出token加进Header
数据库导入中文乱码连接串没指定utf8mb4字符集Django配置中OPTIONS加"charset": "utf8mb4",MySQL表也设为utf8mb4

6.2 两个需要专门说说的深坑

第一个是时间序列的数据泄露问题。我见过有同学的预测模型在测试集上MAE只有3分,我可高兴了,但后来发现他的特征里包含了"当年实际分数线"自己——他把目标字段total_score当成特征输进去了。自查方法很简单:模型做完后,把特征的列名列表打印出来,人工确认目标字段不在其中。这种低级错误一旦自查出,是很好的答辩素材,因为它说明你理解数据语义,而不是只会调库。

第二个坑是预测目标的定义要统一。有的同学预测"总分线",有的预测"复试最低录取分",这两个概念完全不同——复试线是官方划的进复试的门槛,而录取最低分是实际录取的最低成绩,两者之间往往有差值。如果你的历史数据混用了这两种口径,模型学出来的就是一个四不像。我做项目时在数据导入脚本里增加了一条强校验:同一院校同一专业同一年份只能有一条分数线记录,且total_score字段的值必须筛选在350到420之间的正常区间,超出区间的直接进异常报告等待人工确认。这个校验逻辑很简单,却守护了整条模型的底。

6.3 调试技巧与效率心得

分享三个提升调试效率的心得。

第一,Django的print大法并不过时。很多同学一遇到问题就去查日志框架,但对于毕设规模的项目,在view里加几行print(request.data)比配置loguru快得多。真正上线之前再把print清理掉就行。不要有心理负担,工具是为效率服务的。

第二,接口自测用Postman或Apifox,不要每次都开着前端页面点。后端开发阶段,我就用Apifox保存了所有接口的请求模板,参数改一改就能重新测试,不用等前端把页面搭好。前后端并行开发本来就是分离架构的优势,你得会利用它。

第三,数据库备份用Django的dumpdata。模型改来改去很容易把数据弄乱,我会在每次做完重要修改后执行一次python manage.py dumpdata > backup.json。数据丢了重新爬、重新录入的代价太高,备份成本却低到可以忽略。

7. 一些实际的扩展想法

预测和推荐主流程做完后,如果还有精力,我建议挑下面两三个方向做扩展,不用全做。

方向一是加入复试与调剂信息模块。复试名单、拟录取名单、调剂缺额、调剂成功率,这些数据在考研场景里的价值甚至比初试分数线更高。可以让用户按院校查看历年复试比对近三年复试线与录取最低分的差距,这个差距能直观反映学硕复试刷人是否严重。

方向二是把预测做成对比模式。用户可以选择两到三所目标院校,并排展示预测区间、近五年走势、报录比变化,帮助用户在纠结"到底报A还是B"的场景下做更理性的决策。A和B在这个场景下用某院校A、某院校B代替即可。

方向三是导出报告。把用户的基础画像、预测结果、推荐院校列表、推荐理由等汇总生成一份PDF报告,用户可以下载保存。这个功能用Python的reportlab或者前端用html2canvas转PDF都能实现,工作量不大,但演示效果非常完整,会让人觉得整个系统的闭环是思考过的。

最后说一句我自己带毕设时经常跟学生讲的话:做这个题目,目标不是"做出一个无比精确的分数线预测",而是"用一套清晰的技术方案把数据流动起来"——数据从爬虫到数据库、从特征工程到模型、从接口到前端图表,每一步都有逻辑、有验证,这本身就是一篇合格的毕业设计论文的骨架。而预测精度上的不完美,恰恰是你展示误差分析、优化思路的空间。所以如果你正卡在某个环节,别慌,把链路拆细,一步一步来。

我在实际做这类项目时还有个体会:推荐列表里永远要放几个"意料之外、情理之中"的院校。完全按规则打分,结果容易过于保守;在做推荐策略时我会故意给某些特征加上一点随机探索系数,让结果里出现一两个用户可能没听过但匹配度其实很高的学校。这种"惊喜感"在演示时效果很好,也给了用户探索新选项的理由。你也可以在项目里设计一个类似的探索参数,然后主动向导师解释这个设计背后的意图——这是答辩加分项。

祝你把题目做成一个能真正讲出故事的作品。

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

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

立即咨询