ARTICLE DETAIL

资讯详情

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

Python+Vue全栈开发自助选课系统:从需求分析到部署上线

Python+Vue全栈开发自助选课系统:从需求分析到部署上线 这几年用Python做课设、毕设的人越来越多其中“python基于vue的大学生自助选课系统”算是出现频率极高的一类全栈需求。后端用Django或Flask前端用Vue开发工具选PyCharm几乎成了标准组合。今天这篇内容不打算讲那种“只跑通一个页面”的演示项目而是把一个真正能用的自助选课系统从需求拆解、环境搭建、后端接口、前端页面到联调部署的完整过程捋一遍。选课系统的核心其实不复杂学生登录、看课表、选课、退课系统自动检查时间冲突和人数限制。但越简单的业务越容易在并发抢课、跨域请求、前后端字段对不上这些细节上翻车。我会把实际开发中踩过的坑一并写出来正在做这个方向的人可以直接照着抄。1. 整体设计与技术选型先拆清楚需求再动手写代码1.1 核心需求拆解课程、选课记录和冲突检测任何项目上手第一步都不是新建文件夹而是把业务规则一条条列清楚。大学生自助选课系统里角色大概分两类学生和管理员。学生看课程列表、按自己时间选课、退课、查看已选课程管理员维护课程信息、设置课程容量、查看选课统计。这是最基础的中小型MIS系统模型。把业务规则翻译成技术规则后真正需要注意的约束条件是同一学生不能选择上课时间重叠的两门课程每门课程有选课人数上限满了就不能再选学生可以退课退课后释放名额系统需要记录选课时间方便后续统计这些规则看起来简单但落到数据库设计和接口逻辑时每一步都要对应好。比如时间冲突检查课程表里不能只存一个“上课时间”字符串而要用星期几加节课/时间段的方式存储或者至少存两个字段开始节次和结束节次。否则后续做冲突判断会非常痛苦。我在实际项目里还见过有人把上课时间存成“周一 3-4节 5-6节”这种带中文的文本结果接口里没法做任何比较最后只能改表结构重来。所以第一步要把数据字典定清楚。1.2 为什么选Python VueDjango和Flask如何取舍选课系统这种业务用Python写后端效率很高。Python生态里最常用的两个Web框架就是Django和Flask很多人纠结怎么选。我的判断标准很简单如果你需要快速看到完整后台管理功能并且希望ORM、迁移、表单、认证这些现成的能力比较齐首选Django。如果你只想做轻量级API服务喜欢自己搭结构想更自由地控制代码组织那Flask更合适。对比项DjangoFlask内置ORM有功能强大无需要搭配SQLAlchemy自带后台Admin有非常方便无需自己开发或扩展项目结构固定models/views/urls分层明确灵活自己决定怎么组织上手难度中等概念多但统一较低核心就是路由和视图适合场景快速开发、后台管理多、模板渲染轻量API、微服务、高度定制有一段时间FastAPI也很火性能好、自带接口文档适合纯API场景但选课系统里Django的Admin后台确实能省下大量管理端开发时间。甚至管理员课程维护这个模块用Django Admin改几行配置就够了。Flask的话就得自己写一套管理页面或者单独做个前端管理界面。我这次主讲的路径是Django作为主力后端因为它的ORM和事务管理在选课这种高并发场景下更顺手。但Flask版本涉及到的关键接口逻辑我也会一并写上原理是通用的。前端选Vue 3配上Vue Router做页面跳转、Pinia做状态管理、Axios发请求。用Vue写这种交互型页面比传统的模板渲染体验好太多了组件化开发也让课程卡片、选课列表这些部分可以被复用。1.3 数据表设计和项目目录结构规划选课系统的数据表核心就是三张学生Student、课程Course、选课记录Enrollment。学生表主要字段有学号、姓名、所属专业课程表字段要包含课程名称、课程编号、学分、上课时间、上课地点、授课教师、课程容量、已选人数选课记录表是关联表维护student_id、course_id、选课时间并且要设置联合唯一索引防止同一学生重复选同一门课。课程表里的“上课时间”字段建议不要存单字段文本而是拆成day_of_week星期几和start_section、end_section第几节到第几节这样冲突检查就是纯数值比较。比如“周一第3-4节”对应day_of_week1、start_section3、end_section4。这个设计是整个败笔的高发点一定要提前想好。项目目录结构我习惯拆成backend和frontend两个独立目录。后端是Django项目内部有apps/courses、apps/users这些模块前端是Vue工程下面有src/views、src/components、src/api。前后端分离开发时两个项目各跑各的端口联调时Django跑在8000Vue跑在5173代理转发解决跨域。后面章节会详细说这个。2. 环境准备PyCharm里配置Python和Vue三个核心点2.1 PyCharm创建项目与Python虚拟环境写Python项目PyCharm是我长期在用的IDE。选课系统这个项目建议用PyCharm Professional版本因为Django和Vue的支持都更完整。启动后先创建新项目选择Django模板或者纯Python项目都可以。如果用Django模板PyCharm会自动生成manage.py、settings.py这些文件如果用纯Python项目后面再自己执行django-admin startproject也没问题。虚拟环境一定要用起来。很多初学者直接使用全局Python环境装了一堆包后版本冲突到怀疑人生。PyCharm里新建项目时会默认帮你创建一个虚拟环境不过是用venv还是conda可以看你的习惯。我一般用venv简单干净。创建完环境后在PyCharm的Terminal执行python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Mac/Linux pip install django flask-cors # 按需装这里顺带回答一个高频问题PyCharm里装第三方库失败怎么办比如有人想装pandas、装htmltestrunner直接点右下角的包管理器装不上时就去Settings - Project - Python Interpreter看解释器路径是否选对了以及pip是否更新。命令行里执行python -m pip install --upgrade pip基本能解决大多数包装不上的问题。2.2 前端Vue环境与依赖安装Vue开发环境的核心是Node.js。去Node官网下载LTS版本安装装完自带npm。然后校验一下版本node -v npm -v用Vite创建Vue 3项目是最快的npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 pinia axios npm run dev为什么不用Vue CLI因为Vue官方已经转向Vite了Vite启动速度快、配置也直观尤其适合前后端分离项目。有人搜索“vue安装及环境配置”“vue安装依赖”大概率卡在网络或者Node版本上。如果npm安装依赖非常慢可以切换淘宝镜像源npm config set registry https://registry.npmmirror.com依赖装完之后工程结构里就能看到src/components和src/views后面页面组件就往这里放。2.3 初始化Django应用并确认依赖清单后端初始化不要着急写代码先把依赖和项目结构搭好。Django项目执行django-admin startproject config . python manage.py startapp users python manage.py startapp courses这里有个小坑PyCharm的Django模板生成的目录不一定符合你的预期最好手动确认settings.py里是否把新增的app都注册到了INSTALLED_APPS。users和courses都要注册。然后装跨域支持的库Django处理跨域很方便pip install django-cors-headers在settings.py的INSTALLED_APPS加上corsheaders中间件加上corsheaders.middleware.CorsMiddleware再设置允许跨域来源CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]如果你用的是Flask后端对应的处理方式是安装flask-cors然后CORS(app)一行就能解决但因为它的第三方包比较轻需要自己把跨域配置做细。这一段准备工作的精髓是环境一定一次弄干净别等到联调时再回头补依赖那是白白浪费时间。3. 后端API实现从课程表编写到抢课的冲突检测3.1 Django模型定义与数据迁移后端核心是模型层也就是数据库存什么字段、表之间怎么关联。以Django为例我把courses/models.py里的Course模型简化成下面这样from django.db import models class Course(models.Model): name models.CharField(max_length100, verbose_name课程名称) course_code models.CharField(max_length20, uniqueTrue, verbose_name课程编号) credit models.FloatField(default2.0, verbose_name学分) teacher models.CharField(max_length50, verbose_name授课教师) day_of_week models.IntegerField(choices[(i, f周{i}) for i in range(1, 8)], verbose_name星期) start_section models.IntegerField(verbose_name开始节次) end_section models.IntegerField(verbose_name结束节次) capacity models.IntegerField(default30, verbose_name容量) selected_count models.IntegerField(default0, verbose_name已选人数) class Meta: db_table course def __str__(self): return self.nameEnrollment模型class Enrollment(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, verbose_name学生) course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name课程) enroll_time models.DateTimeField(auto_now_addTrue, verbose_name选课时间) class Meta: db_table enrollment unique_together (student, course)selected_count字段是冗余字段可以理解为“课程已选人数”它必须和Enrollment的真实记录数保持一致。为什么要加这个字段因为选课时如果每次都要count()一遍Enrollment表在高并发下既慢又容易出现逻辑漏洞。冗余字段配合下面要讲的事务和锁才能保证容量不超。写完后执行python manage.py makemigrations python manage.py migrate这一步就是所谓“django执行查询-删除对象”的起点所有增删改查都建立在迁移成功后干净的表结构上。3.2 选课接口与时间冲突检测逻辑选课接口是系统的灵魂。接口逻辑不只是“往Enrollment表里insert一条”而是要顺序完成校验课程存在、校验课程是否有余额、校验时间冲突、写入记录、更新已选人数。下面是我常用的Django视图写法from django.db import transaction from django.db.models import Q from rest_framework.decorators import api_view from rest_framework.response import Response api_view([POST]) transaction.atomic def enroll_course(request): student request.user.student_profile course_id request.data.get(course_id) course Course.objects.select_for_update().get(idcourse_id) # 容量检查 if course.selected_count course.capacity: return Response({code: 1, msg: 人数已满}) # 时间冲突检查 conflict Enrollment.objects.filter( studentstudent, course__day_of_weekcourse.day_of_week, course__start_section__ltcourse.end_section, course__end_section__gtcourse.start_section, ).exists() if conflict: return Response({code: 1, msg: 上课时间冲突}) Enrollment.objects.create(studentstudent, coursecourse) course.selected_count 1 course.save(update_fields[selected_count]) return Response({code: 0, msg: 选课成功})时间冲突的判断条件看起来短其实很关键。我判断重叠区间用的就是两个区间相交的数学规则旧课开始 新课结束且新课开始 旧课结束。只要符合这个条件两个时间段就一定有重叠。因为我已经把课程时间拆成了星期几和节次两个维度day_of_week先过滤然后用节次区间做重叠判断非常高效。这里还要注意一个细节为什么选课创建和人数更新放在一个事务里因为如果先创建Enrollment再更新人数中间发生异常时会出现记录和人数不一致。transaction.atomic保证两步要么全成功要么全失败。如果是Flask版本一样可以用SQLAlchemy的session配合with db.session.begin():来包事务。核心不是框架而是事务边界。3.3 并发抢课时防止超选和重复选选课系统最容易暴露问题的场景就是抢热门课。很多人同时点选课如果不做任何并发控制容量30的课程可能被选进去50人数据库里也没有约束能拦住。这就是为什么我上面用了Course.objects.select_for_update()。select_for_update()是数据库层面的行锁。事务A拿到课程行锁后事务B在同一个时间段就必须等待A提交后B才能继续。这样B读到selected_count时A已经更新过了容量检查自然能拦住超选。在MySQL的InnoDB引擎下这个锁是真实有效的。不过使用行锁要注意事务里别做耗时的外部请求比如调用其他接口、发邮件否则把锁挂太久会拖垮并发。对于选课这个场景锁住课程行的范围很小性能基本可以忽略。从业务设计上还可以再加一道兜底在数据库里用唯一索引studentcourse挡住重复选课。“唯一约束兜底 业务逻辑判断”是最稳妥的组合。4. Vue前端页面开发从路由配置到课程卡片展示4.1 Vue Router与页面结构设计前端页面结构我划分为三个主要视图登录页、课程列表页、已选课程页。后台管理可以再加一个课程管理页。Vue Router把这些视图串起来import { createRouter, createWebHistory } from vue-router; const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /courses, component: () import(../views/CourseList.vue) }, { path: /mine, component: () import(../views/MyCourses.vue) }, ]; const router createRouter({ history: createWebHistory(), routes, }); export default router;懒加载就是用() import()的方式让每个页面独立打包首屏加载更快。如果只是课设不懒加载也不是什么大问题但用上这个习惯挺好。4.2 组件拆分与插槽的实用场景前端页面如果全写在一个文件里几百行代码堆在一起维护体验非常糟糕。我会把课程卡片抽成一个组件CourseCard.vue然后在列表页用v-for渲染。这里顺便说一个高频搜索词“vue插槽”的实际用途。卡片组件里有些区域是固定的比如课程名、教师、时间但卡片底部的操作按钮会因状态变化而不同有时候显示“选课”有时候显示“已选”还有管理员的“编辑”。这种场景非常适合用slot插槽template div classcourse-card h3{{ course.name }}/h3 p{{ course.teacher }} · 周{{ course.day_of_week }} 第{{ course.start_section }}-{{ course.end_section }}节/p slot nameaction/slot /div /template使用组件时CourseCard v-forcourse in courseList :keycourse.id :coursecourse template #action button clickhandleEnroll(course)选课/button /template /CourseCard这样页面结构清爽复用性也高。如果课程有视频预览或者教学资源需要播放前端组件里还可以放一个video标签配合hls.js播放m3u8流——比如嵌入课堂回放或课程宣传片。虽然自助选课系统不一定需要这个功能但作为扩展加分项很值得了解。4.3 Axios请求封装与状态管理选课页面交互的核心动作是异步请求。Axios直接在每个页面里写会很乱我习惯把请求统一封装到src/api/request.jsimport axios from axios; const request axios.create({ baseURL: /api, timeout: 10000, }); request.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) config.headers.Authorization Bearer ${token}; return config; }); export default request;接口调用示例import request from /api/request; export function enrollCourse(courseId) { return request.post(/enroll, { course_id: courseId }); }页面里拿到响应后统一处理比如选课成功刷新列表失败则用消息提示框把后端返回的msg展示给用户。全局状态用Pinia来管理学生信息、选课列表这些跨页面共享的数据。比如登录后将用户信息扔进store后续页面从store里读取避免每个页面都请求一次用户接口。5. 前后端联调与启动全流程跨域、代理和接口验证5.1 跨域问题与Vite代理配置前后端分离开发时一个跑5173端口一个跑8000端口浏览器直接发请求会被CORS策略拦住。虽然我在后端设置过CORS_ALLOWED_ORIGINS但更推荐的方法是在前端开发服务器里做代理让Vue的请求看起来像是同一个源发出的。在Vite项目根目录打开vite.config.jsexport default { server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, }, }, };这样前端请求/api/courses在开发环境下会被转发到http://localhost:8000/api/courses。代理的好处是前端代码里不需要写完整的带端口地址后面生产部署切换环境也更方便。5.2 构造测试数据与接口调试联调前后最烦人的事情就是没有数据可调。我一般会写一个Django management command或者直接用Django Admin后台往数据库塞测试数据。Django Admin在这里的优势就体现了注册Course模型from django.contrib import admin from .models import Course admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display [name, teacher, day_of_week, start_section, end_section, capacity, selected_count]开发阶段直接用Admin后台创建几门模拟课程不同时间段的课程数据准备几组测试冲突检测时就能看到效果。再用Postman直接调用后端接口验证POST http://localhost:8000/api/enroll/ Body: {course_id: 1}看返回结果是否符合预期。也建议用Postman把退课接口、课程列表接口都调一遍确认字段名称前后端一致。我见过很多联调问题最后发现就是前端传了courseId后端却期望course_id这种低级错误一旦出现排查起来特别耗时间。5.3 从PyCharm启动Django和Vue的完整流程PyCharm里启动后端很简单直接把项目里的manage.py配置成一个Django Server运行配置端口设置为8000。启动后终端会显示Starting development server at http://127.0.0.1:8000/。前端则需要在PyCharm的Terminal窗口里进入frontend目录执行npm run dev然后浏览器访问http://localhost:5173登录后就能看到前端的课程列表。这里有个体验优化可以在PyCharm里新增一个npm运行配置指向dev脚本这样点一下绿三角自动启动前端。两个服务都跑起来后整个联调环境就通了。6. 常见问题与排查技巧把这几个典型的坑提前给你6.1 环境安装类问题速查现象原因解决办法pip install 超时或下载慢默认官方源慢换清华/阿里镜像源或用-i参数npm install 一直卡住npm官方源不稳定配置registry.npmmirror.comPyCharm无法识别Vue语法未安装Vue.js插件Settings - Plugins 搜索Vue.js安装python命令不是内部或外部命令Python未加入环境变量安装时勾选Add Python to PATHnode -v成功但npm -v失败Node版本异常重装Node LTS版本有个细节值得单独提醒如果你在PyCharm里用虚拟环境新开终端一定要确保虚拟环境已激活。否则明明pip装过的包运行项目时找不到多半就是解释器没选对。PyCharm右下角可以切换Python解释器这个是排查环境问题的第一入口。6.2 业务逻辑类问题速查选课人数容量不同步手动通过数据库改过selected_count或者直接在表里插了Enrollment记录没走接口。解决办法是检查接口里是否用事务包住创建记录和更新数量两步并定期用SQL统计核对。时间冲突检测不生效检查课程表里是否把上课时间存成了中文文本或者day_of_week的值没有对应上。还有一个小坑如果只比较start_section和end_section但漏掉了“星期几”条件就会出现周一和周三的课程互相冲突的假阳性。Django查询-删除对象时报错退课场景中删除Enrollment记录时外键关联的学生或课程记录如果已经被删除Django的on_deleteCASCADE会导致联级删除。如果不想物理删除可以在表中加一个is_active字段做软删除这样日志和统计都不受影响。6.3 部署与交付注意事项如果是要交课程设计或准备答辩强烈建议把项目启动过程写成 README环境依赖锁文件固定下来。后端用requirements.txt前端用package.json。另外不要硬要把前后端部署到同一个服务上。生产环境可以把npm run build生成的dist目录交给Djangostaticfiles托管但更清晰的做法是后端API部署到一个端口前端静态文件部署到Nginx并配置反向代理。选课这种小型系统一个人维护起来反向代理方案最省心。最后说点个人体会这类型全栈项目最大的价值不是炫技而是在小规模场景里把数据库设计、事务边界、异步请求和组件化开发这几个基本功全部过一遍。我实际写完这个选课系统之后最大的收获是理解了“越简单的业务越考验细节”这句话。尤其是那些在并发选课中暴露出来的问题几乎都是因为一开始没有想清数据表之间的约束关系。建议你在开发前先花半小时把表和接口捋清楚写代码也就一两天的功夫。如果中途卡住优先查后端日志和浏览器Network面板那种到处瞎试代码的效率是最低的。
返回列表