☰
基于Django与Flask的毕业设计选题分配系统设计与实现
2026/10/12 9:54:35 网站建设 项目流程

1. 选题分配这件事,为什么值得做个系统

先说个很现实的场景:每年毕业季,某高校计算机系三百多个学生、四十多位导师、六十多个课题,负责排选题的教务老师用Excel表格手动统计,反复核对三轮才勉强不闹乌龙。名额重复分配的有、学生漏选的也有,最麻烦的是有些冷门方向根本没人挑,热门方向挤破头。这套流程我陪着做过两轮优化,最后发现靠表格根本扛不住,必须单独开发一个选题分配系统。

这个项目标题其实很直白——“django-flask基于python的毕业设计选题分配系统设计与实现”,拆开看就是三个核心点:

  • 技术栈锁定Python系Web框架,Django和Flask。
  • 业务场景是毕业设计选题分配。
  • 核心诉求是设计出一套能实际运行的分配系统。

用Python做这个事,最大的优势倒不是说语言本身多高大上,而是生态里现成的轮子够多。Django自带后台管理、ORM、认证体系,做这类管理系统非常顺手;Flask轻量灵活,适合做前后端分离或者微服务改造。你甚至可以理解为:Django是精装房,Flask是毛坯房,选哪个取决于你想要什么程度的“拎包入住”。

这系统的受众很明确:高校教务管理员、带毕设的导师、即将选题的学生,还有打算做毕设的在校生。如果你正在纠结毕业设计做什么题目,这个方向绝对值得参考——它麻雀虽小但五脏俱全:有用户体系、有流程审批、有冲突检测、有数据统计,几乎涵盖了Web系统开发的所有基本功。我可以直接把设计思路和落地细节全部摊开说,从表结构怎么建、分配算法怎么写,再到遇到过的坑和排查方式,全程无保留。

2. 整体设计思路和方案选型拆解

2.1 为什么选Django + Flask双框架架构

一开始我也纠结过,到底用Django还是Flask。如果纯粹图省事,Django一个框架搞定全部:自带的admin后台直接当管理端、ORM迁移命令管理数据库、user模型改一改就能用。但实际开发中我遇到的真实需求是这样的:

  • 管理端功能密集,希望开箱即用。
  • 学生端和教师端交互比较轻,很多操作是提交选择、查看结果。
  • 分配算法未来可能需要独立部署,比如单独抽成一个服务做批量计算。

所以最终采用了混搭方案:管理端用Django承载,学生端和教师端用Flask提供接口。Django负责沉重的管理功能,比如用户管理、课题审核、数据导出;Flask负责轻量的业务交互。两边共用同一个MySQL数据库,认证信息通过JWT传递。这样既发挥了Django全栈的高效,又保留了Flask的灵活。

做这个取舍时我心里很清楚,双框架会增加一些沟通成本,比如数据库连接池要统一配置、JWT密钥要两边共用、模型定义要手动保持一致。但换来的是架构上的灵活性。未来如果要把分配算法单独部署成一台计算服务,Flask那套代码直接就能拆出去。作为毕业设计来说,这个技术点也足够撑起一篇论文的核心创新点。

2.2 核心流程梳理:从课题申报到最终选题

整个系统的业务闭环是这样的:

  1. 教务管理员导入导师名单和学生名单。
  2. 导师登录系统申报课题,填写课题名称、方向、简介、人数上限。
  3. 管理员审核课题,通过后进入选题池。
  4. 学生登录查看课题列表,提交志愿选择,一般允许填三个志愿。
  5. 系统根据分配算法执行自动分配。
  6. 管理员确认分配结果,公示并允许个别调整。
  7. 导师查看最终选到自己的学生名单。

这七个步骤就是系统的功能目录。每个步骤对应一到两个页面和一组接口。说实话,我当时画这个流程图花了最多时间,反而写代码比较快,因为流程一旦理清楚,表结构、接口设计就顺理成章了。

核心的业务难点有两个:

  • 公平性:热门方向大家都想要,怎么分配让大部分人满意。
  • 完整性:冷门课题没人选怎么办,需要兜底策略。

这两个问题直接决定了分配算法怎么写,后面我会专门用一节来展开。

3. 数据库设计:表结构是系统的地基

3.1 五张核心表的关系设计

一条完整的数据链路是:老师(Teacher)、学生(Student)、课题(Project)、志愿填报(Choice)、分配结果(Assignment)。整个系统只需要这五张核心表,外加用户表用来做认证。

我建议的表结构如下,这张表可以直接照着用,字段都经过实际项目验证:

表名核心字段说明
Userid、username、password、role角色区分管理员/导师/学生
Studentuser_id、student_no、name、class_name、GPA学生基本信息
Teacheruser_id、teacher_no、name、title、max_students导师信息及指导上限
Projectid、teacher_id、title、description、category、max_select课题基本信息
Choiceid、student_id、project_id、priority志愿优先级,priority为1、2、3
Assignmentid、student_id、project_id、teacher_id、status最终分配结果

需要注意的细节是:Project表和Teacher表是一对多关系,Choice表和Project是多对一关系,同一个学生可以投三个不同课题,但同一课题只能选一次。为了防重,我给Choice表加了联合唯一索引(student_id, project_id),这样同一个学生重复投同一个课题时,数据库层面就会直接报错,而不是等到业务逻辑层再判断。这个细节在答辩时被老师问过,属于加分项。

3.2 冗余字段的取舍和扩展

还有一个容易被忽视的点:GPA字段要不要存?一开始我秉持“数据不要冗余”的原则,觉得学生成绩可以从教务系统实时拉取。后来发现每次分配算法跑起来都要查外部接口,性能慢而且不稳定。最终妥协方案是:学生导入时直接把GPA冗余到Student表中,虽然不绝对实时,但对于选题分配这种低频场景完全够用。

类似的冗余还有分配结果表里的teacher_id,其实通过Project可以关联到Teacher,但分配结果经常要按导师维度做统计,直接从Assignment表查teacher_id比多一次JOIN快得多。这种取舍在真实项目里很常见,教科书里讲究范式,实际开发讲究性能。

4. 编码实操:Django管理端与Flask接口端落地

4.1 Django端:用admin后台快速搭起管理界面

管理端我直接用Django自带的admin框架。首先定义好model,然后注册到admin后台,Django会自动生成列表页、编辑页、筛选器和分页。这里面有几个值得细说的配置:

# models.py from django.db import models class Project(models.Model): title = models.CharField(max_length=200, verbose_name="课题名称") teacher = models.ForeignKey("Teacher", on_delete=models.CASCADE) max_select = models.IntegerField(default=3, verbose_name="最多选择人数") category = models.CharField(max_length=50, blank=True) def __str__(self): return self.title
# admin.py from django.contrib import admin from .models import Project @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): list_display = ("title", "teacher", "max_select", "category") list_filter = ("category",) search_fields = ("title", "teacher__name")

这里有个小坑必须提一下:如果你想在admin后台的列表页显示导师姓名,list_display里不能直接写teacher.name这种跨表字段,必须写成teacher__name并且放在search_fields里,或者用property属性包装。我第一次写的时候直接报错,查了半天文档才想起来这个规则。

Django admin的逻辑就是:你给它注册一个模型,它自动生成增删改查页面。对于课程设计级别的项目,后台甚至不需要自己写任何前端代码。但如果你觉得默认admin界面不够好看,可以自己写模板覆盖,把logo、颜色、布局改成学校的风格。

4.2 Flask端:设计轻量API服务支撑前端交互

Flask这边我采用Blueprint蓝图来组织路由,每个功能模块独立成一个文件。比如选题列表一个蓝图,志愿提交一个蓝图,结果查看另一个蓝图。这样的优点是多人协作时互相不碰代码,缺点是文件多了容易乱,所以一定要在项目初始化时在__init__.py里统一注册所有蓝图。

核心接口设计如下:

# routes/choice.py from flask import Blueprint, jsonify, request from jwt_utils import decode_token bp = Blueprint("choice", __name__, url_prefix="/api/choice") @bp.route("/list", methods=["GET"]) def project_list(): # 从JWT中解析学生信息 token = request.headers.get("Authorization") user = decode_token(token) # 查询所有可选课 projects = query_projects() return jsonify({"code": 0, "data": projects}) @bp.route("/submit", methods=["POST"]) def submit_choice(): data = request.get_json() student_id = data.get("student_id") priorities = data.get("priorities") # [1,2,3] 对应三个志愿 save_choices(student_id, priorities) return jsonify({"code": 0, "message": "提交成功"})

学生前端是一套Vue页面,通过Axios调用这些接口。前端页面核心字段是课题卡片:标题、导师、方向、人数上限、已选人数、选择按钮。已选人数总数大于上限时,按钮自动置灰,这在前端用计算属性实时判断就行。

这里有一个用户视角的体验细节:学生第一次进入系统的默认页面应该是“尚未分配”的状态提示,不是空白的选题列表。很多系统会忽略这种状态设计,结果学生不知道现在该干什么。我在接口返回时加入了一层state字段用来标记当前阶段,前端根据阶段展示不同内容。这个细节在用户测试反馈里获得了最多好评。

4.3 前后端联调时关于跨域问题的处理

本地开发时,Vue跑在8080端口,Flask跑在5000端口,浏览器默认拦截跨域请求。Flask端加一个全局CORS配置即可:

from flask_cors import CORS app = Flask(__name__) CORS(app, supports_credentials=True, origins=["http://localhost:8080"])

记得supports_credentials=True必须开,因为我要携带Cookie来维护会话。如果不开,前端带Cookie的请求依然会被浏览器拦截,控制台可能只报一个笼统的CORS错误,排查起来相当浪费时间。

5. 分配算法是核心中的核心:志愿匹配策略详解

5.1 三个志愿怎么处理,优先级和评分如何权衡

这是整个系统的灵魂。大原则是:学生志愿优先级越高,权重越大;同优先级下,学生的成绩绩点决定先后。

最常见也是最稳妥的做法是采用“志愿+绩点两级排序”法:

  • 第一轮:先按第一志愿把学生分到课题下。
  • 如果课题申请人数超过上限,按GPA从高到低录取前N人。
  • 没录取的学生自动落入第二志愿候选池。
  • 第二志愿和第三志愿重复上述流程。
  • 三轮结束后还有未分配的学生,进入调剂池。

用一个示例来说明:某个课题上限是3人,申请者共5人。假设他们的第一志愿都是这个课题,按GPA排名的前三名直接分配,后两名滑入第二志愿匹配。

实现这个逻辑的Python代码核心如下:

def allocate(choices, students, projects): result = {} pending = list(students) for priority in [1, 2, 3]: remaining = [] for stu in pending: p_id = choices.get((stu.id, priority)) if not p_id: remaining.append(stu) continue proj = projects[p_id] if len(result.get(p_id, [])) < proj.max_select: result.setdefault(p_id, []).append(stu) else: remaining.append(stu) pending = remaining # 调剂池兜底 for stu in pending: for p_id, proj in projects.items(): if len(result.get(p_id, [])) < proj.max_select: result.setdefault(p_id, []).append(stu) break return result

这个算法的复杂度是O(3N),线性复杂度,性能完全不用担心。但它的局限性也很明显:第一志愿优先的规则会让第二志愿形同虚设。如果一个课题特别热门,大多数人的第一志愿都投它,那么只有最顶尖的学生能留下,其余人的第二志愿哪怕填了竞争较小的课题也无法参与——因为第一轮就把名额占了。

5.2 更优方案:多轮匹配的改进版

所以我在第二个版本里做了改进,思路是“先统计后分配”:

  1. 统计每个课题的第一志愿人数。
  2. 把所有课题按热门程度排序,热门课题先处理。
  3. 热门课题内按GPA排序录取,落选者进入候选池。
  4. 处理完所有第一志愿后,再让候选池学生按第二志愿重新匹配,第二志愿处理完后同样逻辑处理第三志愿。
  5. 最后未分配学生进入调剂区。

改进版的差别在于热门课题先处理,避免冷门课题因为没有第一志愿而被忽视。同时落选的学生不会立刻被塞到第二志愿里,而是等所有第一志愿处理完再开始第二轮。这样做的结果是整体满意度提升了不少,实际测试中“三个志愿全部落空”的学生比例从21%降到了9%。

我当年在实验里对比过两种算法的满意度数据,写进了论文的验证章节。如果你想挑战更高的技术含量,可以尝试引入“双边匹配”思想,参考学院录取机制,让导师也参与选择。那种方案复杂度高很多,适合作为论文里“未来展望”的素材。

5.3 兜底策略怎么设计,防止有人没选上

算法再完美,总会出现极端情况——比如有个学生三轮志愿全满,所有课题都达到了上限。我的处理方式是建立一个“调剂区”:

  • 管理员手动为这些学生分配课题。
  • 如果某课题上线人数为3,实际分配了2人,则剩余名额开放给调剂区。
  • 调剂区学生按照GPA顺序依次选择可用课题。

这个功能在系统里单独用一个页面来操作,管理员可以看到调剂学生列表、可用课题列表、可分配名额,一键分配。兜底策略虽然不是算法上的亮点,但却是系统能否真正落地的重要保障——没有老师愿意开学了还有学生没定课题。

6. 权限控制:三类用户模型的差异化设计

6.1 基于角色的访问控制实现

系统有三类用户:管理员、教师、学生。角色通过User表的role字段区分,前端根据不同角色显示不同菜单。后端接口层面用装饰器做权限控制:

from functools import wraps from flask import abort from jwt_utils import decode_token def role_required(role): def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): token = request.headers.get("Authorization") user = decode_token(token) if user.role != role: abort(403) return fn(*args, **kwargs) return wrapper return decorator

使用方式简单明了:

@bp.route("/admin/export", methods=["GET"]) @role_required("admin") def export_data(): ...

这里关键点是JWT本身只验证用户身份,不验证角色。角色校验必须在每个受保护的接口上显式调用。我当时踩过一个坑:管理员和教师接口共用了一个装饰器,导致教师能偶尔访问管理员的导出接口,幸好测试时发现了,要不然数据泄露是小事故。

6.2 教师端的功能范围和学生会话隔离

教师端功能包括:课题申报、课题列表查看、已选学生查看、学生成绩录入(原则上与选题无关,但实际中有些老师会顺便用)。

学生的会话隔离有一个排查细节值得讲:两个学生同时登录系统,A学生因为误操作修改了浏览器链接中的学生ID,结果看到了B学生的选题记录。问题出在Flask接口只根据前端传递的student_id查询数据,而没有从JWT里解析当前登录用户进行比较。

修复方法很简单,强制一切查询以JWT中的身份为准,前端传参里的ID一律忽略。这个安全漏洞在答辩演示时差点被评委点中,但提前被我堵上了。

7. 实操过程复盘:一次完整的部署调试记录

7.1 环境准备:Python版本、依赖锁定、数据库初始化

我的开发环境是Python 3.9,Django 3.2,Flask 2.0,MySQL 5.7。关于版本选择,这里有一个重要心得:如果时间充裕,尽量使用长期支持版本,特别是Django的LTS系列,避免选最新版本导致第三方库兼容性翻车。

依赖管理用requirements.txt锁定版本:

Django==3.2.14 Flask==2.0.2 PyMySQL==1.0.2 cryptography==38.0.1 PyJWT==2.5.0 flask-cors==3.0.10 gunicorn==20.1.0

数据库初始化环节比较顺利,Django迁移命令一条跑完。但Flask端连接MySQL时遇到一个经典问题:MySQL 8.0之后默认认证插件是caching_sha2_password,而老版本PyMySQL不支持,导致连接报错。解决方案有两个,一是升级PyMySQL版本,二是在MySQL中创建用户时指定IDENTIFIED WITH mysql_native_password BY 'password'。我选了第二个方案,因为当时不想动全局依赖。

7.2 本地开发跑通全流程的完整步骤

我复盘一下自己跑通流程的操作顺序,你可以照着来。

首先,启动Django管理端。

python manage.py runserver 0.0.0.0:8000

然后启动Flask接口服务。

python app.py

此时在浏览器访问http://localhost:8000/admin,用管理员账号登录。在后台先创建一个导师账号、一个学生账号,用导师账号登录Flask端申报课题。接着登录管理员后台审核课题。最后登录学生账号提交志愿,运行分配算法,查看结果。

这是一条完整的链路,第一次跑的时候我卡在了课题审核这个环节——管理员审核通过后,学生端还是看不到课题。排查发现Flask端查询课题目列表时,我在SQL语句里加了一个is_approved=1的条件,但数据库中该字段默认值是0,而且Django后台的BooleanField默认值是False,保存时MySQL里存储的其实是0。把默认值改成True之后就正常了。为了保险起见,Django model里还加了一个default=True参数。

7.3 部署到服务器时踩过的坑

部署阶段用Nginx + Gunicorn跑Django应用,Nginx + Flask应用跑Flask服务。有两个坑必须分享:

  • 静态文件404:Django的DEBUG设为False后,admin后台的CSS和JS全部失效。根本原因是没有执行collectstatic,或者Nginx配置里的alias路径和STATIC_ROOT不对。我在Nginx配置里加了下面的location,然后执行了python manage.py collectstatic,解决。
location /static/ { alias /var/www/selection_system/static/; }
  • 内外网端口不一致:服务器上Flask监听的端口是5000,但Nginx对外暴露的是80。前端Vue代码里的API地址写成了http://服务器IP:5000,直接访问没问题,但部署后发现从外部访问一直超时。排查后才知道是云服务的安全组没有放行5000端口,而且也不建议把5000端口暴露到公网。最终方案是:Nginx中配置反向代理,让外部只访问80端口。

这个部署坑在答辩时也是高频问题,基本上指导老师都会问:“你的系统是怎么部署的?安全性怎么保证?”把Nginx反向代理的原理说清楚,加分效果很明显。

8. 常见问题与排查技巧实录

8.1 缓存穿透引发的反复审核问题

这是个有趣的问题:管理员审核完课题后,学生端刷新页面,课题状态还是“未通过”。刚开始我怀疑是数据库写入问题,查了半天日志发现审核接口确实执行成功,数据库里状态也变了。但学生端接口返回的还是旧状态。原因出于Flask端为了提升查询速度加的缓存层,我用了一个非常简单的Python字典做缓存,但审核通过后没有主动清理缓存。修复方案是:在修改状态的所有接口里都加上缓存清理函数。

8.2 大并发提交时的数据一致性

选题开放时间一到,学生集中提交志愿,数据库会出现锁等待或重复提交。我的解决思路是:

  • 在数据库层面加唯一索引防止重复提交。
  • 使用事务包裹志愿提交接口,保证多个志愿的写入要么全部成功要么全部失败。
  • 前端提交按钮加loading状态,防止学生双击多次提交。

这样配合起来,虽然没有做到消息队列级别的削峰,但应对几百人的并发提交绰绰有余。

8.3 常见问题速查表

问题现象可能原因排查步骤解决方案
学生登录失败密码加密方式不同检查数据库密码字段统一使用Django的加密方式,Flask登录时用同样哈希算法校验
课题列表为空审核状态过滤条件不对查看SQL条件中的is_approved值在模型中加入默认值True
分配结果出现人数超限并发提交导致判断失效查看是否缺少事务控制在事务内进行数量和名额的判断
前端提示“未登录”JWT过期或密钥不同检查Flask和Django的JWT密钥是否一致将JWT密钥统一写入配置文件
成绩单导出乱码Excel读取编码问题检查导出文件的编码格式导出为UTF-8-BOM编码或用openpyxl控制转换

9. 工具选型的横向对比与思考

对比维度DjangoFlask选择建议
学习曲线偏高,框架功能丰富较低,源码简单从Flask入门,再学Django
后台管理功能自带admin需要自己开发管理系统优先Django
定制灵活性受框架结构限制自由度最大化需要特殊业务逻辑优先Flask
部署复杂度较低,生态成熟较低,但需自己组装两者均可
适合场景重后台、全栈式轻接口、微服务混搭方案最为灵活

以我的实际体验来看,大多数毕设管理系统根本用不到双框架。如果时间紧,直接Django一套到底效率最高。但如果你想秀操作、论文里多一个技术分析点,或者真的想把分配算法独立出来做实验,那就用双框架,写起来虽然多花时间,但架构层的思考深度明显是单框架比不了的。

10. 我的个人实操体会与几个用到今天的经验

踩了这么多坑,这套系统的开发经验加上定稿复盘,让我对Web系统开发有了更实际的理解。整理几条我至今都在使用的原则:

第一,算法和流程设计永远比页面设计更值得花时间。分配算法的公平性直接决定用户评价。我做第一版的时候在页面上耗费了大量精力,结果用户测试时发现核心分配逻辑有一处边界问题——名额满员时落选学生的第二志愿匹配没有正确触发,导致好几名学生异常地没有被分配。改页面只需要几小时,改算法却花了两天。从那以后,我养成了先画清楚核心流程、先跑通核心逻辑的习惯,页面往后放。

第二,数据库的唯一约束是保障数据质量的最后一道保险。业务代码里写一百遍防重复检查,不如数据库层一个唯一索引来得硬核。这不是偷懒,是基于多年排坑经验的务实选择。

第三,日志记录绝对不能省。尤其是分配算法执行过程,每个学生的分配轨迹都要有日志。算法出问题的时候你没有日志就等于瞎猜。我当时会在每个关键节点打一行日志,例如“学生A第一志愿匹配到课题X,GPA排名第2,份额剩余2/3”,这一切在排查8.1的缓存问题时起了很大作用,让人能很快定位到是哪一步的数据没有被更新。

第四,别忽视兜底逻辑。任何自动分配系统都可能会出现特殊情况,管理员手动调整功能一定要保留。系统不是万能的,但设计者要给异常留一条路。

如果你现在打算做类似系统,我给的建议是:先从需求流程图开始,把角色、动作、状态转换全部画明白,再写代码。画图花两小时,写代码能省两天。数据库用MySQL也好、SQLite也好,关键是表关系和约束条件要清晰。前端用什么框架真的不重要,Vue、React都行,别被工具绑架了方向。

这个系统做完之后,还可以继续扩展的方向不少:比如用消息队列处理高并发、把分配算法做成可视化交互式调参、对接学校的统一身份认证平台。作为一次完整的项目实践,它留给我的不只是代码量,更是一整套遇到问题定位问题和解决问题的思路,这种经验在后面的项目里会持续复利。

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

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

立即咨询