ARTICLE DETAIL

资讯详情

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

Django高考志愿填报系统设计与实现:从数据模型到部署全指南

Django高考志愿填报系统设计与实现:从数据模型到部署全指南 简介这是一份基于Python语言与Django框架的高考志愿填报系统毕业设计完整资料适合计算机类专业的毕业设计、课程设计及项目演练。资源涵盖系统开发全流程包含项目源码、数据库设计脚本、详细设计文档与全部配套资料可帮助读者快速理解系统架构、业务逻辑与数据库表结构。压缩包整体约214MB代码已在Windows及macOS环境下运行通过可直接部署调试或作为二次开发基础。详细文档结合需求分析、总体设计、系统实现与测试等环节展开方便对照源码进行梳理尤其适合需要在较短时间内完成高质量毕设的学生。该资源已有125人学习下载是经过实际评审的成熟方案能够节省大量从零搭建的时间也为后续功能扩展和毕业答辩提供了可靠参考。1. 别把高考志愿填报系统做成CRUDDjango毕设的真实考点每年毕业季计算机、软件工程、信息管理类专业里基于PythonDjango的选题占了相当比例高考志愿填报系统又是其中最容易“做得像但是没做对”的一类。大多数同学拿到“设计与实现”这几个字第一反应就是搭一个发布院校列表的网站再配上增删改查就算交差。实际上这类系统的核心考点不在页面而在三件事数据模型能不能支撑按分数、位次、省份、科类做组合查询志愿提交时有没有考虑幂等和并发以及冲稳保推荐是写死了规则还是真正基于历史录取位次计算出来的。这篇内容适合正在做毕设、或者想拿Django做Web课程设计的开发者我会按照数据建模、业务逻辑、管理后台、部署验证的顺序把这套高考志愿填报系统从头到尾讲清楚。代码可以直接抄参数会逐个解释踩坑点也会点出来。2. 数据模型与选型志愿填报系统的表怎么设计才扛得住查询2.1 用Django ORM建模从院校、专业到招生计划的三个核心表高考志愿填报系统里最忌讳的就是把一切都塞进一张大表。院校有院校的属性专业有专业的属性招生计划每年都会变志愿填报则是用户行为数据。我一般会先建四个核心模型College、Major、EnrollmentPlan、Volunteer。前三个是基础数据第四个是用户提交的志愿单。from django.db import models class College(models.Model): name models.CharField(院校名称, max_length100, uniqueTrue) province models.CharField(所在省份, max_length50, db_indexTrue) city models.CharField(所在城市, max_length50, blankTrue) level models.CharField(院校层次, max_length20, choices( (985, 985), (211, 211), (dual_first, 双一流), (ordinary, 普通本科) ), defaultordinary) tags models.CharField(院校标签, max_length200, blankTrue, help_text用逗号分隔如理工类,公办) created_at models.DateTimeField(创建时间, auto_now_addTrue) def __str__(self): return self.name class Meta: ordering [name] verbose_name 院校 verbose_name_plural 院校 class Major(models.Model): college models.ForeignKey(College, on_deletemodels.CASCADE, verbose_name所属院校, related_namemajors) name models.CharField(专业名称, max_length100) category models.CharField(学科门类, max_length50, blankTrue, help_text如工学、理学、管理学) subject_requirement models.CharField(选科要求, max_length100, blankTrue, help_text如物理化学) def __str__(self): return f{self.college.name}-{self.name} class Meta: verbose_name 专业 verbose_name_plural 专业 unique_together (college, name)招生计划表是查询频率最高的表。考生输入分数后系统要按省份、科类、年份过滤出可报院校和专业所以这张表的索引设计直接决定接口响应速度class EnrollmentPlan(models.Model): college models.ForeignKey(College, on_deletemodels.CASCADE, verbose_name院校, related_nameplans) major models.ForeignKey(Major, on_deletemodels.CASCADE, verbose_name专业, related_nameplans) year models.PositiveIntegerField(招生年份, db_indexTrue) province models.CharField(招生省份, max_length50, db_indexTrue) subject_type models.CharField(科类, max_length20, choices( (physics, 物理类), (history, 历史类), (art, 艺术类), (sports, 体育类) ), db_indexTrue) plan_count models.PositiveIntegerField(计划招生人数) min_score models.PositiveIntegerField(最低录取分数, nullTrue, blankTrue) min_rank models.PositiveIntegerField(最低录取位次, nullTrue, blankTrue, db_indexTrue) tuition models.DecimalField(学费, max_digits8, decimal_places2, default0) def __str__(self): return f{self.year} {self.college.name} {self.major.name} class Meta: verbose_name 招生计划 verbose_name_plural 招生计划 indexes [ models.Index(fields[province, subject_type, year]), ]这里有个很关键的设计min_score和min_rank同时存在但业务计算时要优先使用min_rank。原因很简单每年试题难度不同600分在不同年份的含金量差异很大而位次是相对稳定的。按位次去做冲稳保判断比按分数靠谱得多。EnrollmentPlan里加了一个联合索引(province, subject_type, year)这个组合就是志愿筛选最基础的条件查询频率最高、选择性最好索引放这里收益最大。另外要留意models.Index和db_indexTrue的区别前者适合声明复合索引后者用于单字段索引。用户提交的志愿表需要单独建模型因为一个用户可以填报多所学校每所学校可以填多个专业中间还要记录调剂意愿class Volunteer(models.Model): user models.ForeignKey(auth.User, on_deletemodels.CASCADE, verbose_name考生用户) plan models.ForeignKey(EnrollmentPlan, on_deletemodels.CASCADE, verbose_name志愿项) priority models.PositiveIntegerField(志愿顺序, help_text1表示第一志愿) is_major_adjust models.BooleanField(是否服从专业调剂, defaultFalse) created_at models.DateTimeField(填报时间, auto_now_addTrue) updated_at models.DateTimeField(修改时间, auto_nowTrue) class Meta: verbose_name 志愿填报记录 verbose_name_plural 志愿填报记录 unique_together (user, priority)unique_together (user, priority)表示同一个用户不能重复使用同一个志愿顺序号。这个约束是必须的否则用户就能提交两个第一志愿业务上说不通。2.2 为什么优先选MySQL而不是SQLite数据量、并发和部署习惯很多初学者图省事直接在settings.py里用默认的SQLite理由是“本地跑得通就行”。对于高考志愿填报系统这个选题我建议提交前把数据库换乘MySQL。原因有三个表数据量是一方面。一份相对完整的招生计划数据至少覆盖近三年、全国两千多所院校、几百个专业记录数轻松超过十万。SQLite在十万级数据的简单查询上还能应付但一旦带上联表比如筛选专业时同时关联院校信息性能下降非常明显。并发是更重要的理由。SQLite的写锁是数据库级别的一个请求在写志愿记录时其他请求的写操作就会被阻塞。答辩演示时如果同时打开几个页面提交志愿页面很可能直接报database is locked。MySQL在并发写场景下用的是行级锁不会出现这种尴尬。第三个理由其实和部署有关。很多人的最终环境是宝塔面板MySQL组合本地开发用SQLite、线上用MySQL相当于维护两套配置迁移数据还要做一次导出导入纯属给自己找事。一步到位用MySQL写settings.py时也方便统一管理。配置方式如下DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: gaokao, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }需要先安装mysqlclient才能连上MySQLpip install mysqlclientOPTIONS里的charset必须设置为utf8mb4否则专业名称里的生僻字在写入数据库时会被\ufffd替换。sql_mode设置STRICT_TRANS_TABLES是为了在字段超长、类型错误时直接抛异常而不是静默截断方便排查脏数据。2.3 Django执行查询-删除对象的边界QuerySet的惰性与缓存陷阱Django ORM 里最容易被忽略的就是 QuerySet 的惰性求值。查询集在创建时不会访问数据库只有被迭代、切片、调用list()、判断布尔值时才真正执行SQL。这个特性有好有坏坏就坏在你会无意识地重复查询。典型的踩坑场景是这样的plans EnrollmentPlan.objects.filter(province广东省, year2024) # 第一次用到时不执行SQL if len(plans) 0: print(有数据) # 上面这一行已经执行了一次COUNT查询 # 下面循环又执行一次完整查询 for plan in plans: print(plan.min_score)len(queryset)会先触发一次查询后面的 for 循环触发第二次查询两次SQL一摸一样数据量上来之后就是白白的开销。更隐蔽的是删除对象的操作。很多人不知道QuerySet.delete()是直接执行DELETE语句不走模型的delete()方法因此也不会触发信号和级联行为之外的额外清理逻辑。# 方式一直接过滤删除不走模型方法 EnrollmentPlan.objects.filter(year2020).delete() # 方式二先取对象再逐个删除会触发每个实例的delete() plans EnrollmentPlan.objects.filter(year2020) for plan in plans: plan.delete()方式一适合批量清理过期数据效率高方式二适合在模型里重写了delete()方法做扩展逻辑时的场景。注意Django默认的on_deletemodels.CASCADE在底层数据库也会生成外键级联所以删除一个College对象时关联的Major和EnrollmentPlan会被一并删除使用时要非常小心。如果不想级联删除就改用models.PROTECT。3. 从空目录到能跑通的志愿填报流程Django视图里的业务逻辑3.1 用django创建app并配置URL最小可运行的项目骨架拿到一个毕业设计项目不要先急着看页面先把项目跑起来再说。这里用命令行创建django-admin startproject gaokao_system cd gaokao_system python manage.py startapp recommendrecommend这个app负责志愿推荐、查询、填报业务。创建完成后需要把它注册到settings.py的INSTALLED_APPS中然后在gaokao_system/urls.py里include子路由from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(recommend.urls)), ]recommend/urls.py里的结构这样写把视图和URL分清楚from django.urls import path from . import views urlpatterns [ path(search/, views.search_schools, namesearch_schools), path(plan/int:year/str:province/str:subject_type/, views.plan_list, nameplan_list), path(volunteer/submit/, views.submit_volunteer, namesubmit_volunteer), path(volunteer/list/, views.my_volunteers, namemy_volunteers), ]注意plan_list的URL里直接带了三个路径参数year、province、subject_type这样搜索引擎友好的同时前端不用在GET请求里额外拼参数。视图函数可以直接从路径里解包省去了一步参数校验from django.shortcuts import render, get_object_or_404 from .models import EnrollmentPlan def plan_list(request, year, province, subject_type): plans EnrollmentPlan.objects.filter( yearyear, provinceprovince, subject_typesubject_type ).select_related(college, major) return render(request, recommend/plan_list.html, {plans: plans})select_related是这里最值得解释的参数。它会在一条SQL里通过JOIN把college和major关联的数据一起查出来避免在模板里循环访问plan.college.name时触发N1查询。十行数据看不出区别五万行数据的时候这个差异直接就是秒级和毫秒级的区别。3.2 位次换算与“冲稳保”判定在视图层做一次真实计算高考志愿填报系统里最核心的业务并不是增删改查而是给用户一个可解释的志愿建议。冲稳保的判定逻辑必须基于位次而不是分数。核心思路是用户输入位次user_rank然后去比较每所学校历年最低录取位次min_rank。两者的关系先理一遍位次数字越小代表排名越靠前。如果学校的历年最低录取位次是5000你的位次是4000说明你排在这所学校去年录取的最低排位考生前面录取概率较大。如果学校的历年最低录取位次是3000你的位次是4000说明去年录取的最后一名都比你靠前这个学校录取概率就很低。所以“冲”的意思是用户的位次数字大于学校录取位次数字但差距不大“稳”是用户位次略小于或等于学校位次“保”是用户位次远小于学校位次预留足够余量。def classify_school(user_rank, school_min_rank): if school_min_rank is None: return 未知 ratio user_rank / school_min_rank if school_min_rank 0 else 1 if ratio 0.85: return 保 elif ratio 1.05: return 稳 elif ratio 1.25: return 冲 else: return 风险较高ratio的含义是用户位次除以学校最低录取位次。比值小于1表示用户排位在学校录取线之上越大越危险。比值在0.85以下归为“保”说明成绩相对学校往年录取位次有超过“15%位次余量”。而这个 15% 的阈值不是绝对的不同省份、不同年份可以调整。我会把阈值放到一个可以配置的地方比如数据库里的SysConfig表或者写死在settings.py中VOLUNTEER_STRATEGY { stable_ratio: 1.05, # 比值低于该值算“稳” challenge_ratio: 1.25, # 比值高于该值算“冲” safe_ratio: 0.85, # 比值低于该值算“保” }这样做的目的是让这套系统具备可配置性。答辩时演示给老师看老师问“你这个冲稳保的阈值是怎么定的”你就能说出“阈值是参考湖北省、广东省近三年录取数据设置的并且支持在配置表中调整”这比说“我是凭感觉写的”要有说服力得多。3.3 志愿提交的幂等性避免重复提交和并发覆盖用户点击“提交志愿”按钮时如果网络卡顿他可能会连点两次这就会产生两份一模一样的志愿记录。更严重的是两个并发请求同时修改同一个用户的同一条志愿记录时可能出现覆盖。最简单的幂等方案是在表单里加入一个token字段。首次进入填报页面时生成一个随机串存在session里提交时核对import secrets from django.shortcuts import redirect from .models import Volunteer, EnrollmentPlan def submit_volunteer(request): if request.method POST: token request.POST.get(token) if token ! request.session.get(volunteer_token): return redirect(submit_volunteer) plan_id request.POST.get(plan_id) priority request.POST.get(priority) is_adjust request.POST.get(is_major_adjust) on Volunteer.objects.create( userrequest.user, plan_idplan_id, prioritypriority, is_major_adjustis_adjust ) # 一次性token使用后立即失效 del request.session[volunteer_token] return redirect(my_volunteers)这里每一步都值得解释secrets模块是Python 3.6之后的标准库生成的随机串是密码学安全的不要用random模块替代。del request.session[volunteer_token]必须在创建记录之后执行这样第二次请求进来时token已经失效自然就被拦下。这个方案解决的是“同一用户重复提交”的问题。但还不够。如果两个请求同时到达先创建priority1的志愿记录又会有一个请求以priority1再次创建记录导致违反唯一约束。Django的unique_together会让数据库抛出IntegrityError用 try-except 消化异常并提示用户“该志愿顺序已被使用”from django.db import IntegrityError try: Volunteer.objects.create( userrequest.user, plan_idplan_id, prioritypriority, is_major_adjustis_adjust ) except IntegrityError: # 提示用户更换志愿顺序 pass # 这里在真实代码里要返回校验错误信息为了缓解“多条志愿记录同时提交时的顺序被打乱”问题可以引入事务行锁。Django里使用select_for_update()对用户加锁from django.db import transaction transaction.atomic def submit_volunteer_batch(request, volunteer_items): user request.user with transaction.atomic(): locked_user type(user).objects.select_for_update().get(pkuser.pk) for item in volunteer_items: Volunteer.objects.create( userlocked_user, plan_iditem[plan_id], priorityitem[priority], is_major_adjustitem[is_major_adjust] )宏观上讲select_for_update()会在数据库层面锁定这一行直到事务提交。其他请求操作同一用户时就会等待锁释放。这套逻辑放在毕设里是超纲的亮点但注意select_for_update()只支持MySQL、PostgreSQL这类事务型数据库SQLite下会静默失效。4. 让毕设看起来像产品检索、筛选和后台管理的实用增强4.1 在列表页组合筛选按省份、科类、选科要求过滤考生使用系统时第一件事是输入分数和省份然后筛选“专业科目要求”。这里我一般用Q对象做动态条件组合。假设考生选科是“物理化学生物”能报的专业要求有两种不提科目要求和物理或化学作为必选。但不同专业的选科规则比较复杂最稳妥的做法是先把规则定义在模型里然后在查询端用多值匹配from django.db.models import Q def search_schools(request): qs EnrollmentPlan.objects.select_related(college, major) province request.GET.get(province) subject_type request.GET.get(subject_type) requirement request.GET.get(requirement, ) if province: qs qs.filter(provinceprovince) if subject_type: qs qs.filter(subject_typesubject_type) if requirement: # 例如 requirement物理 qs qs.filter( Q(major__subject_requirement__icontainsrequirement) | Q(major__subject_requirement__iexact不限) ) return render(request, recommend/search_result.html, {plans: qs})这段代码有两个细节。icontains和iexact都会走索引吗答案是都不一定。icontains底层是LIKE %物理%前导通配符会破坏B树索引生效。但在这里仍然有业务价值当用户在搜索框中输入“物联网”时icontains可以把“物联网工程”“物联网应用技术”全部捞出来。既然牺牲了性能就要限制使用场景这类模糊搜索建议加最少两个精确过滤条件比如省份和科类来收敛结果集。4.2 Django admin界面美化让答辩演示不再像半成品Django自带的admin界面在毕业设计答辩里其实是非常加分的前提是你别用它默认的样子。不需要写复杂前端代码通过几个属性和小小的CSS定制就能让后台管理看起来像一个正式的管理系统。先看admin.py的配置from django.contrib import admin from .models import College, Major, EnrollmentPlan, Volunteer admin.register(College) class CollegeAdmin(admin.ModelAdmin): list_display [id, name, province, level, tags] list_filter [province, level] search_fields [name, tags] admin.register(EnrollmentPlan) class EnrollmentPlanAdmin(admin.ModelAdmin): list_display [id, college, major, year, province, subject_type, min_score, min_rank] list_filter [year, province, subject_type] search_fields [college__name, major__name] list_per_page 50 admin.register(Volunteer) class VolunteerAdmin(admin.ModelAdmin): list_display [user, plan, priority, is_major_adjust, created_at] readonly_fields [created_at, updated_at]list_filter会让右侧出现一个过滤器面板在这里按年份、省份、科类快速筛选。search_fields里用了college__name这种跨表查询写法admin会自动处理JOIN。list_per_page设置50条一页数据量大时避免加载卡顿。外观上再改一步新建templates/admin/base_site.html{% extends admin/base_site.html %} {% block branding %} h1 idsite-name a href{% url admin:index %}高考志愿填报系统管理后台/a /h1 {% endblock %}Django admin的默认头图是“Django administration”改成自己的系统名称后整个后台的专业感会提升不少。如果再想进一步定制可以引入一个开源admin主题比如django-simpleui安装后就自带侧边栏和现代风格代码量几乎为零。4.3 如果做前后端分离用DRF暴露接口而不是渲染模板模板渲染方案对毕业设计来说省时省力但如果论文里写了“前后端分离架构”代码里却全是Django Template技术上就站不住脚。我这里提供一个最小可用的DRFDjango REST Framework实现覆盖“搜索接口→返回JSON→前端渲染”这条路。首先安装依赖并注册到INSTALLED_APPSpip install djangorestframework序列化器和视图写成这样from rest_framework import serializers from rest_framework.views import APIView from rest_framework.response import Response from .models import EnrollmentPlan class PlanSerializer(serializers.ModelSerializer): college_name serializers.CharField(sourcecollege.name) major_name serializers.CharField(sourcemajor.name) class Meta: model EnrollmentPlan fields [id, college_name, major_name, year, province, subject_type, min_score, min_rank] class PlanSearchAPI(APIView): def get(self, request): qs EnrollmentPlan.objects.select_related(college, major) province request.query_params.get(province) if province: qs qs.filter(provinceprovince) serializer PlanSerializer(qs[:100], manyTrue) return Response(serializer.data)PlanSerializer里的college_name serializers.CharField(sourcecollege.name)是DRF的核心用法。source告诉序列化器去取plan.college.name。这样前端拿到的JSON不是college对象的嵌套结构而是扁平的college_name字段对于直接渲染表格非常友好。qs[:100]是硬限制防止前端一次拿几十万条记录。接口参数要和前端的axios请求严格对齐fetch(/api/plans/?province湖北省subject_typephysics) .then(res res.json()) .then(data { const rows data.map(item { return tr td${item.college_name}/td td${item.major_name}/td td${item.min_score}/td td${item.min_rank}/td /tr; }); document.getElementById(result-body).innerHTML rows.join(); });DRF默认的渲染格式是JSON但如果前端出现了跨域问题需要安装django-cors-headers并配置白名单否则浏览器会拦截API响应。5. 部署、演示数据与验证技巧5.1 本机快速造一套可信的演示数据毕设最后展示时最怕的就是数据库里只有三条测试数据点开哪个页面都是空荡荡的。至少要保留一份包含近三年、5个省份、500所以上院校的演示数据集。用Django的Fixture机制最省事python manage.py dumpdata recommend.College recommend.Major recommend.EnrollmentPlan demo_data.json把数据文件放到fixtures/目录下在别的环境导入时只需要一条命令python manage.py loaddata demo_data.json注意dumpdata导出的数据里包含自增主键id所以导入前要保证目标数据库是空的否则会主键冲突。如果你不想手动造数据也可以写一个management/command脚本读取CSV批量导入。Excel里整理好院校列表和历年录取位次然后通过脚本清洗后写入数据库这个操作比手输500条数据要快得多。5.2 用宝塔部署django时的三个容易出错的地方宝塔面板是常见的Django部署工具但很多人在部署阶段卡住最常见的原因有三个。第一个是Python环境问题和依赖安装位置。宝塔的Python项目管理器会打包一个独立的虚拟环境因此在面板里执行pip install -r requirements.txt不代表你的Django应用能读到这些依赖。必须在项目的虚拟环境目录下执行/www/server/pyporject/项目名_venv/bin/pip install -r requirements.txt。第二个是静态文件配置。Django生产模式下不提供静态文件服务需要在settings.py里设置STATIC_URL /static/ STATIC_ROOT /www/wwwroot/gaokao_system/static然后执行python manage.py collectstatic把admin后台的CSS和JS收集到指定目录。如果漏了这一步后台页面会加载不出样式界面一片混乱。第三个是允许主机配置。Django默认只能通过ALLOWED_HOSTS []访问localhost直接部署到外网IP时无法访问。但要视为安全边界不能简单写ALLOWED_HOSTS [*]。我一般只填入自己的服务器IP和域名ALLOWED_HOSTS [120.25.x.x, gaokao.example.com]5.3 验证“冲稳保”结果是否合理的自查脚本准备一份只包含500条测试数据的搜索条件和结果然后写脚本模拟50个位次不同的考生统计每个考生得到的冲稳保数量分布。from recommend.models import EnrollmentPlan def validate_strategy(user_rank, province, subject_type): plans EnrollmentPlan.objects.filter( provinceprovince, subject_typesubject_type ).filter(min_rank__isnullFalse) result {冲: 0, 稳: 0, 保: 0, 风险较高: 0} for plan in plans: ratio user_rank / plan.min_rank if ratio 0.85: result[保] 1 elif ratio 1.05: result[稳] 1 elif ratio 1.25: result[冲] 1 else: result[风险较高] 1 return result # 模拟5000位考生 print(validate_strategy(5000, 湖北省, physics))输出结果里“保”的数量至少要有10个“稳”的数量至少要有8个“冲”的数量在5个以上否则说明演示数据里招生计划的历年录取位次分布不合理需要调整阈值系数。最后把这份脚本名称留成scripts/verify_strategy.py放进项目根目录答辩时让老师看到你验证过数据分布而不是打开页面随口讲。本文还有配套的精品资源点击获取
返回列表