资讯详情

资讯详情

Python Django + Vue打造校园招生管理系统:数据库设计到前后端部署全解析

搞校园招生管理系统这个题目我第一反应就是又是个典型的毕设/课设题目。但真做起来才发现这个项目其实非常有代表性它不是一个简单的CRUD堆砌而是把学生端、招生办后台、数据统计、报名流程串在一起的完整业务闭环。技术栈上选了Python Django Vue这套组合说明你至少已经意识到前后端分离是现在的标配而不是像以前那样用Django模板硬渲染。这篇文章就围绕这个项目完整过一遍从需求拆解、技术选型逻辑到数据库设计、后端接口实现、前端页面搭建再到联调部署和常见坑。文字尽量口语化代码和思路都给你照着改就能用。1. 项目定位与核心模块拆解1.1 校园招生系统到底在管什么事招生管理系统不是简单地把报名信息存进数据库。我梳理了几个核心角色和对应需求这是建模和写代码之前必须想清楚的。学生/家长端查看招生简章、专业介绍、在线填写报名信息、上传证件照和成绩单、查询录取状态、打印报名表。招生办管理员端维护招生计划、审核报名材料、安排面试/考试批次、录入成绩、确认录取、统计各专业报名人数。系统管理层账号分配、角色权限、操作日志、数据备份。这些需求拆完之后你就明白这系统核心是报名流程的状态机。从待审核到已通过/已驳回再到已录取/未录取每一步都对应不同的业务规则。写代码时如果只是弄个status字段没有把状态流转规则想清楚后面一定会改到崩溃。1.2 技术栈选型的真实逻辑标题里写了django和flask很多人纠结到底用哪个。我给一个比较直接的建议这类管理系统首选Django原因不是Flask不好而是这个项目的核心痛点已经被Django内置解决了。Django自带Admin后台意味着招生办的工作人员可以直接用后台做基础数据维护不需要你额外写一堆页面。Django的ORM在处理学生报名、专业、班级、成绩这类关系型数据时非常自然尤其联表查询和条件筛选写起来比裸SQL要安全高效得多。Flask的优势是轻量灵活但灵活性在这个项目里反而是负担用户认证、表单校验、分页、Admin这些Flask都需要自己拼装或者依赖第三方插件。Vue这边则是纯前端渲染负责做报名表单、数据表格、状态看板。前后端通过API通信Django只负责提供JSON数据接口。PyCharm作为IDE没有争议专业版对Django和Vue的支持都很完整社区版也能凑合但专业版的前端调试和数据库工具会省很多事。2. 数据库设计与核心模型定义2.1 最关键的几张表我实际设计时没有一上来就建十几张表。核心就五张其余都是在演进过程中补充的。第一张是专业表存招生计划。字段包括专业名称、所属院系、招生人数、学制、学费、报名开始结束时间、备注。第二张是学生报名表这是整个系统的核心。字段包括学生姓名、性别、身份证号、联系电话、毕业学校、高考分数/会考成绩、报考专业外键、报名状态、审核意见、创建时间。这里要注意身份证号建议加密存储至少也要做脱敏展示别直接在前端回显完整身份证。第三张是审核记录表用来存每一次审核操作的痕迹。字段包括报名外键、审核人、审核结果、审核意见、操作时间。这张表的意义在于招生办在后期纠纷排查时能清楚看到谁在什么时间做了什么决定。第四张是用户表。Django内置的User表可以直接扩展通过OneToOne关联一个Profile里面存手机号、所属角色、是否启用等。注意不要直接改内置User表字段扩展是最稳妥的。第五张是录取结果表字段包括报名外键、录取专业、录取批次、通知书编号、是否确认、确认时间。这张表和报名表分开是因为一个报名可能会有多次录取调整比如调剂专业独立存更灵活。2.2 Django模型实现示例以下是我实际写的一段核心模型代码你可以参考from django.db import models from django.contrib.auth.models import User class Major(models.Model): name models.CharField(专业名称, max_length100) department models.CharField(所属院系, max_length100) quota models.IntegerField(招生人数, default0) duration models.CharField(学制, max_length20, default三年) start_date models.DateField(报名开始时间) end_date models.DateField(报名结束时间) is_active models.BooleanField(是否开放报名, defaultTrue) class Meta: verbose_name 招生专业 verbose_name_plural verbose_name def __str__(self): return self.name class StudentProfile(models.Model): GENDER_CHOICES ((M, 男), (F, 女)) STATUS_CHOICES ( (pending, 待审核), (approved, 已通过), (rejected, 已驳回), (admitted, 已录取), (not_admitted, 未录取), ) user models.OneToOneField(User, on_deletemodels.CASCADE, related_namestudent) name models.CharField(姓名, max_length50) gender models.CharField(性别, max_length2, choicesGENDER_CHOICES) id_card models.CharField(身份证号, max_length18) phone models.CharField(联系电话, max_length20) school models.CharField(毕业学校, max_length200, blankTrue) score models.DecimalField(成绩, max_digits6, decimal_places2, nullTrue, blankTrue) major models.ForeignKey(Major, on_deletemodels.SET_NULL, nullTrue, related_nameapplicants) status models.CharField(报名状态, max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: verbose_name 学生报名信息 verbose_name_plural verbose_name class AdmissionResult(models.Model): applicant models.OneToOneField(StudentProfile, on_deletemodels.CASCADE, related_nameresult) major models.ForeignKey(Major, on_deletemodels.SET_NULL, nullTrue) batch models.CharField(录取批次, max_length50) notice_no models.CharField(通知书编号, max_length50, uniqueTrue) is_confirm models.BooleanField(是否确认, defaultFalse) updated_at models.DateTimeField(更新时间, auto_nowTrue)写模型时有几个细节容易踩坑。一是on_delete参数必须显式指定Django 2.0以后强制要求我建议外键关联系统数据时用PROTECT关联用户上传数据时用SET_NULL防止误删主数据把报名记录带崩。二是身份证号别设成uniqueTrue因为数据清洗前可能存在重复或者空值先用逻辑校验代替数据库唯一约束等数据质量稳定了再考虑加约束。2.3 为什么用状态字段而不是独立流程表很多同学设计时会专门建一张流程实例表来跟踪报名进度我觉得在这个项目里过度设计了。招生报名的流程是固定的、线性的用一个status字段加一个审核记录表完全够用。流程化的状态机适合审批节点多、角色复杂、要动态配置流程的场景。招生报名这个场景里节点一共就四五个而且顺序固定用状态字段更直观。查当前进度一条SQL就出来了写前端状态标签也方便。等到后续要做如果考生成绩达到某专业线自动调剂这种规则时再加规则引擎也不迟一开始就上流程引擎只会把自己绕晕。3. 后端API设计与核心逻辑实现3.1 接口设计原则前端用Vue这意味着后端只需要输出JSON。我设计接口时遵循一个原则按页面而非按表设计接口。也就是说前端某个页面需要什么数据后端就提供一个专门接口而不是让前端自己去组合多个接口。举个例子学生报名页需要展示当前开放专业列表、该生已提交的报名信息、审核状态。我就设计了一个/api/dashboard/接口一次性把这三块数据返回def dashboard(request): if request.user.is_anonymous: return JsonResponse({code: 403, msg: 请先登录}) open_majors Major.objects.filter(is_activeTrue).values(id, name, quota, end_date) my_application StudentProfile.objects.filter(userrequest.user).values().first() return JsonResponse({ code: 0, open_majors: list(open_majors), my_application: my_application, role: admin if request.user.is_staff else student })这个接口代码不长但解决了三个页面的数据需求。写接口时别想着通用通用接口往往意味着前端要写一堆判断逻辑出了问题也难排查。每多一个接口前端就少一次拼数据的负担。3.2 登录认证和权限控制招生管理系统必须有角色权限否则学生能进管理后台就乱套了。我用的方案是Django自带session认证加一个简单的前端路由守卫。后端实现用户登录from django.contrib.auth import authenticate, login, logout from django.views.decorators.http import require_POST require_POST def login_view(request): username request.POST.get(username) password request.POST.get(password) user authenticate(usernameusername, passwordpassword) if user is not None: login(request, user) return JsonResponse({code: 0, msg: 登录成功, is_staff: user.is_staff}) return JsonResponse({code: 1, msg: 用户名或密码错误})is_staff字段天然区分了普通学生和管理员。前端拿到这个字段之后在路由配置里做一个全局守卫router.beforeEach((to, from, next) { const isAuthenticated localStorage.getItem(isLogin) true if (to.meta.requiresAuth !isAuthenticated) { next(/login) } else if (to.meta.requiresStaff localStorage.getItem(isStaff) ! true) { next(/) } else { next() } })这里我要特别说一句localStorage里存登录标记只是方便前端控制显示真正的权限校验必须放在后端。我在每个管理接口里都加了装饰器或中间件判断防止有人绕过前端口直接调接口。安全这块宁可多写几行代码也不要偷懒。3.3 报名审核逻辑与并发处理审核功能是招生办最常用的操作。后端收到审核请求后需要做几件事第一步检查当前状态是否为待审核如果不是就直接拒绝防止重复审核第二步更新状态第三步写入审核记录第四步如果审核通过还要检查该专业是否还有名额。这里有一个隐藏bug如果两个管理员同时审核最后两个名额可能会超录。解决方法是使用Django的select_for_update()行锁from django.db import transaction transaction.atomic def approve_application(application_id, operator, comment): application StudentProfile.objects.select_for_update().get(idapplication_id) if application.status ! pending: return {code: 1, msg: 该报名已处理请勿重复审核} major Major.objects.select_for_update().get(idapplication.major_id) admitted_count StudentProfile.objects.filter( majormajor, statusadmitted ).count() if admitted_count major.quota: return {code: 1, msg: 该专业招生名额已满} application.status admitted application.save() ReviewLog.objects.create( applicationapplication, operatoroperator, resultapproved, commentcomment ) return {code: 0, msg: 审核通过}select_for_update()不是银弹但在这个场景里够用了。它把读和写放进同一个数据库事务里锁住这行记录直到事务提交另一个管理员再查这行时就能看到最新状态。实际项目里把名额判断和状态更新放一起并发问题基本就解决了。4. Vue前端实现与交互细节4.1 项目初始化与依赖安装Vue这边我用Vue 3 Element Plus的组合比Vue 2 Element UI新一个版本组件库更现代样式也好看一些。创建项目npm create vuelatest创建时选择Router、Pinia、ESLint其他可以默认。然后安装需要的库npm install axios element-plusaxios用来发HTTP请求element-plus提供表格、表单、弹窗这些现成组件。PyCharm里可以直接在Terminal窗口执行这些命令不要另开系统终端省得环境变量出问题。4.2 报名表单设计与校验报名表单是整个前端最核心的部分。我用Element Plus的el-form加校验规则效果比手写正则舒服得多template el-form :modelform :rulesrules refformRef label-width100px el-form-item label姓名 propname el-input v-modelform.name placeholder请输入姓名/el-input /el-form-item el-form-item label性别 propgender el-radio-group v-modelform.gender el-radio valueM男/el-radio el-radio valueF女/el-radio /el-radio-group /el-form-item el-form-item label身份证号 propidCard el-input v-modelform.idCard placeholder请输入身份证号/el-input /el-form-item el-form-item label报考专业 propmajorId el-select v-modelform.majorId placeholder请选择专业 el-option v-form in majors :keym.id :labelm.name :valuem.id/el-option /el-select /el-form-item el-form-item el-button typeprimary clicksubmit提交报名/el-button /el-form-item /el-form /template校验规则里身份证号用正则校验18位格式同时校验最后一位校验码是否正确。电话号校验11位数字前三位做运营商号段判断。这些规则写在前端只是提升用户体验后端接口里还必须再校验一次前端校验可以被绕过。提交时的逻辑也要注意先通过前端校验然后调用后端接口后端返回成功才跳转到我的进度页面。如果后端返回名额已满或者重复提交要把错误信息原样展示给用户。4.3 管理员数据表格与筛选功能管理员端的核心是数据表格。Element Plus的el-table配合el-pagination做分页筛选条件用el-select和el-date-picker。每次筛选条件变化时重新请求后端而不是在前端做假分页因为数据量大之后前端分页会卡。我实际写的一个查询方法思路async function fetchList(params) { const res await axios.get(/api/application-list/, { params: { page: params.page, pageSize: params.pageSize, majorId: params.majorId, status: params.status, keyword: params.keyword } }) if (res.data.code 0) { tableData.value res.data.data.list total.value res.data.data.total } }后端对应def application_list(request): queryset StudentProfile.objects.all() major_id request.GET.get(majorId) status request.GET.get(status) keyword request.GET.get(keyword) if major_id: queryset queryset.filter(major_idmajor_id) if status: queryset queryset.filter(statusstatus) if keyword: queryset queryset.filter(name__icontainskeyword) paginator Paginator(queryset, per_page10) page paginator.get_page(request.GET.get(page, 1)) data [{ id: s.id, name: s.name, gender: 男 if s.gender M else 女, major: s.major.name if s.major else , score: float(s.score) if s.score else None, status: s.status, created_at: s.created_at.strftime(%Y-%m-%d %H:%M) } for s in page] return JsonResponse({code: 0, data: {list: data, total: paginator.count}})4.4 数据可视化看板录取统计这块我用ECharts画了三个图各专业报名人数柱状图、报名趋势折线图、录取状态饼图。ECharts用起来不复杂关键是后端要把聚合好的数据给前端。Django里用ORM的聚合查询from django.db.models import Count def stats_api(request): major_stats StudentProfile.objects.values(major__name).annotate( cntCount(id) ).order_by(-cnt) status_stats StudentProfile.objects.values(status).annotate( cntCount(id) ) return JsonResponse({ major_stats: list(major_stats), status_stats: list(status_stats) })后端只返回聚合后的数据图表渲染全部交给前端前后端各管各的这样调试起来非常清晰。5. Flask方案对比与迁移思路5.1 什么时候值得换Flask标题里同时出现了django和flask我猜你可能在纠结。我个人的判断标准是这样的如果项目只是给几百人用业务逻辑也不复杂Flask完全可以。Flask的优点是启动快、代码透明调试时能看到每一个环节。但如果你要在一个学期内把这个系统做完还要兼顾前端我建议Django。Flask不是不能用而是很多本该由框架解决的问题要自己造轮子。用户登录Flask需要Flask-LoginORM需要Flask-SQLAlchemy表单校验需要Flask-WTF。这些库本身都很好但组合在一起需要你理解它们之间的配合学习成本其实高于直接学Django。5.2 如果坚持用Flask核心代码长什么样如果导师指定Flask或者你就是喜欢写Flask那种一目了然的感觉核心接口大概长这样from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///enrollment.db db SQLAlchemy(app) CORS(app) class Student(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50)) status db.Column(db.String(20), defaultpending) major_id db.Column(db.Integer, db.ForeignKey(major.id)) app.route(/api/apply, methods[POST]) def apply(): data request.get_json() student Student(namedata[name], major_iddata[major_id]) db.session.add(student) db.session.commit() return jsonify({code: 0, msg: 报名成功})Flask的好处是这段代码你能看懂每一行在干什么适合学习原理。坏处是用户认证、分页、Admin后台都得自己搭。我的建议是如果是毕设用Django省力如果是为了学习和炫技Flask完全OK。6. 联调、部署与PyCharm使用心得6.1 前后端联调的关键配置前后端分离开发最烦的就是跨域问题。我在Django这边用django-cors-headers解决pip install django-cors-headers然后在settings.py里配置INSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOW_ALL_ORIGINS True # 开发环境用 # 生产环境应改为 # CORS_ALLOWED_ORIGINS [http://your-frontend-domain.com]开发阶段CORS_ALLOW_ALL_ORIGINS设成True方便调试上线前务必改成白名单不然任何网站都能往你的接口发请求很危险。6.2 部署方案这个项目的部署我推荐最省事的方案Django后端用gunicorn起服务前端用nginx托管静态文件并把API反向代理到后端。前端先构建npm run build构建产物在dist/目录。Nginx配置大致如下server { listen 80; server_name your_domain.com; # 前端静态文件 location / { root /var/www/enrollment/dist; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 静态资源缓存 location /assets/ { root /var/www/enrollment/dist; expires 7d; } }Django这边用gunicorn启动gunicorn enrollment.wsgi:application -w 4 -b 127.0.0.1:8000-w 4表示4个worker进程对于校园几百人的访问量完全够用。6.3 PyCharm里提高效率的几个设置先说环境隔离。每个Python项目建一个虚拟环境这是基本素养。PyCharm创建项目时选择Virtualenv然后确认当前解释器路径在项目内部。我见过太多人把Django装到全局环境里结果项目换到别的机器上跑不起来。再说前端调试。PyCharm专业版自带的Vue插件支持断点调试但实际用下来不如Chrome的Vue DevTools直观。我习惯的做法是前端代码在PyCharm里写浏览器用Chrome调配合Vue DevTools看组件状态和数据流。PyCharm绑定的是后端断点前端调试交给浏览器。数据库工具方面PyCharm右侧的Database面板可以直接连上Django项目的SQLite或MySQL不用另开Navicat。修改表结构后在面板里就能看效果方便很多。7. 常见问题与排查技巧实录7.1 数据库表没生成Django新手最常见的错误models.py写好了也执行了makemigrations但打开数据库没有表。原因多半是settings.py里的INSTALLED_APPS没有注册你的app。myapp.apps.MyappConfig如果没有这一行Django根本不识别这个app里面的models。检查顺序是先看INSTALLED_APPS再执行python manage.py makemigrations最后migrate。如果还是不行直接把db.sqlite3删掉重新迁移开发阶段数据不重要重来是最高效的。7.2 Vue打包后刷新404问题前端用Vue Router的history模式时刷新页面会404因为Nginx不知道去哪个静态文件找路由。解决方案就是我前面写的try_files $uri $uri/ /index.html;。这一行的意思是找不到指定文件就回退到index.html由前端路由接管。如果你用hash模式就没这个问题URL会变成/#/dashboard这种带井号的格式。个人推荐history模式好看也正规配合Nginx的配置就能解决。7.3 跨域请求失败出现CORS policy报错先在浏览器Network里看OPTIONS预检请求是否返回200。如果没有预检说明请求不是跨域如果有预检但报错检查CORS_ALLOWED_ORIGINS里是不是漏了你的前端端口。开发时习惯性用CORS_ALLOW_ALL_ORIGINS True省心。登录相关的请求不会触发预检但如果用了Authorization请求头就会触发需要后端允许对应请求头。7.4 并发审核超录问题这个问题前面提过使用select_for_update()加事务锁。如果加了锁还是超录检查一下你的MySQL表引擎是否是InnoDBMyISAM引擎不支持行锁select_for_update()会失效。SQLite则部分支持测试时最好用和线上一致的数据库。7.5 图片上传后无法访问学生上传证件照Django默认把文件传到MEDIA_ROOT目录开发阶段能访问生产环境Nginx没配静态代理就图片全挂。解决location /media/ { alias /var/www/enrollment/media/; }然后确认settings.py里MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media一个小细节是文件权限Nginx用户需要读权限不然白屏。上传类功能建议限制文件类型和大小用Pillow校验图片真实格式防止伪装成图片的木马文件。7.6 表单数据保存失败但没有任何提示这种问题最让人抓狂前端显示请求成功后端控制台也没有报错但数据没写入。一个常见原因是Django的CSRF校验。前后端分离模式下如果前端发起POST请求没有带X-CSRFToken头Django会返回403但前端如果没处理这个错误界面就会看起来成功。解决方法是取cookie里的csrftoken在axios请求拦截器里加上axios.interceptors.request.use(config { const token document.cookie.match(/csrftoken([^;])/) if (token) { config.headers[X-CSRFToken] token[1] } return config })比全局关掉CSRF好一万倍因为关掉之后所有POST接口都没防护这是自己给自己挖坑。8. 写在最后的一点个人体会这套系统我完整做完大概用了三周真正写代码的时间不到一半大部分时间花在需求确认和各模块联调上。刚开始需求不清晰连审核不通过后是否可以重新报名这种事都要反复确认你要是自己定规则后面一定会被用户找上门。我觉得这类管理系统的项目经验最有价值的不是CRUD本身而是理解状态这个东西怎么流转、数据怎么组织、权限边界怎么控制。写Django接口的时候每写一个函数先问自己这个接口会被谁调他需要什么数据有没有可能造成脏数据这三个问题想清楚代码自然就稳定了。后面如果你想扩展这个项目可以考虑加一个邮件通知功能审核结果出来后自动给学生发邮件。再进一步可以接一个第三方短信服务报名成功发个短信提醒。这些都是直接在现有模型上加字段、加接口就能实现的不会破坏现有结构。做项目的顺序永远是先把主流程跑通再考虑锦上添花的东西。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →