ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python+Vue教务选课考试系统设计与全栈开发指南

2026/10/6 3:49:51 拓冰建站 浏览量
Python+Vue教务选课考试系统设计与全栈开发指南 毕业设计年年都有人做教务系统但大部分交上来的东西都停留在“能跑”的阶段——后端怼一堆接口前端套个模板老师演示时手动加几条数据糊弄过去。今天这篇就从一个常见的题目说起“python基于vue的教务选课考试成绩系统的设计与实现”从需求拆解到具体落地把选课、考试、成绩这三个核心业务链路完整梳理一遍。无论你准备用django还是flask用pycharm还是vscode这篇都能给你一条可以直接抄作业的路径。适合看这篇的人有三类正在做毕业设计、需要交系统演示的学生想用Pythonvue做第一个完整全栈项目的自学开发者以及工作中突然接到“内部选课/考试管理系统”这类需求、想快速搭个原型的同学。我会把选课模块的并发处理思路、成绩录入的权限控制、考试安排的数据设计都讲清楚附带实际踩过的坑和排查方法争取让你少走几个星期的弯路。1. 项目整体设计与需求拆解1.1 教务选课考试系统的核心业务链路先别急着写代码把业务想明白再做后面至少会顺一半。教务系统的核心链路其实只有三条学生选课、教师录成绩、管理员管一切。围绕这三条链路我们把角色和权限拆开看。学生端核心功能查看可选课程列表、选课/退课、查看已选课程表、查看考试成绩。教师端核心功能查看自己负责的课程、维护考试安排、录入/修改成绩、导出成绩单。管理员端核心功能管理学生/教师账号、管理课程信息、管理选课时间窗口、查看全校选课统计与成绩统计。业务上还有几个容易忽视但毕业答辩老师一定会问的点选课时间冲突怎么处理一门课在选课截止后能否退课成绩提交后教师能否修改还是需要管理员审批。这些在设计阶段就要定清楚不然后面改起来全是眼泪。1.2 为什么前端选Vue、后端选Django/Flask这套组合基本是当前国内毕业设计和中小型项目的标配原因很直接Python写后端相对简单好调试Vue做交互界面很顺手两者通过RESTful API沟通前后端分离之后交给老师演示的时候还可以“一台电脑同时起两个服务”互不干扰。后端框架在Django和Flask之间的选择很多人纠结。我的看法是如果项目里有复杂的权限体系、后台管理界面、ORM想开箱即用选Django。如果就想保持轻量、想灵活安排目录结构或者你未来准备转FastAPI方向选Flask。我这次讲的时候会以Django为主但会在具体环节把Flask写法也点一下。选Django的另一个实际原因是admin自带后台你做管理员功能时可以少写一半的页面。哪怕你用Flask我也建议用Flask-SQLAlchemy做ORM别直接写原生SQL后面改需求时省心太多。1.3 前后端分离架构下的数据流设计前后端分离之后数据是怎么走的这个想清楚接口设计才不会乱。Vue页面选课列表 → 调用 /api/courses/ → Django/Flask路由 → 视图函数处理 → ORM查询数据库 → 返回 JSON → Vue渲染表格在这个流程里后端只负责提供数据和校验规则前端负责渲染与交互。所以我们要先设计好统一的JSON返回格式我建议至少包含三个字段code状态码、message提示信息、data业务数据。所有接口都按这个格式返回前端写起来特别省事一个axios拦截器全部搞定。比如成绩查询接口的返回格式可以是这样{ code: 200, message: id: 1 name: 张三 score: 85, data: { student_id: 1, student_name: 张三, course_name: 数据结构, score: 85 } }2. 数据库设计与核心模型拆解2.1 教务系统涉及的六张核心表教务系统数据模型是整个项目的根基。根子歪了后面写接口就是给烂地基糊墙。我总结下来最精简但能覆盖完整业务流程的表是这六张用户表含学生/教师/管理员、课程表、选课记录表、考试安排表、成绩记录表、学期表。用Django的模型写法来说大概是这样from django.db import models from django.contrib.auth.models import AbstractUser # 1. 用户表扩展 class UserProfile(AbstractUser): ROLE_CHOICES ( (student, 学生), (teacher, 教师), (admin, 管理员), ) role models.CharField(max_length10, choicesROLE_CHOICES, defaultstudent) student_id models.CharField(max_length20, blankTrue, nullTrue) teacher_id models.CharField(max_length20, blankTrue, nullTrue) # 2. 课程表 class Course(models.Model): name models.CharField(max_length100, verbose_name课程名称) code models.CharField(max_length20, uniqueTrue, verbose_name课程编号) teacher models.ForeignKey(UserProfile, on_deletemodels.CASCADE, limit_choices_to{role: teacher}) credit models.FloatField(default2.0, verbose_name学分) capacity models.IntegerField(default50, verbose_name容量) selected_count models.IntegerField(default0, verbose_name已选人数) semester models.CharField(max_length20, verbose_name学期, default2024-2025-1)这里有个细节值得注意课程表的selected_count字段这个字段是冗余计数也就是每次选课后把它加1。为什么不用选课记录表.count()现查因为选课高峰期这条SQL会被疯狂执行统计一次大表非常消耗数据库性能。用冗余字段一次事务直接锁行更新性能好得多。2.2 选课、考试、成绩三张关联表的逻辑关系选课记录表是整个系统的枢纽同时关联用户和课程。这里最需要关注的是唯一约束防止同一个人重复选同一门课。class Enrollment(models.Model): student models.ForeignKey(UserProfile, on_deletemodels.CASCADE, related_nameenrollments) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_nameenrollments) enroll_time models.DateTimeField(auto_now_addTrue) status models.CharField(max_length10, choices((active, 正常), (dropped, 已退课)), defaultactive) class Meta: # 同一个学生在同一门课中只能有一条有效记录 unique_together (student, course)考试安排表要单独建不要挂在成绩表上。因为考试在先、成绩在后可能学生选完课但教师还没安排考试也可能考完试但还没录分。考试表先记录时间地点成绩表再引用考试记录这样逻辑最顺。成绩记录表设计时有一个容易被问到的点平时成绩和期末成绩怎么处理我的建议是拆成两个字段regular_score平时成绩和final_score期末成绩总分在接口层面计算。拆开的好处是教师录入时可以只改平时分不影响期末分。如果你想更灵活可以再加一个JSON字段扩展平时成绩明细比如regular_detail models.JSONField(defaultlist)。2.3 选课冲突与容量校验的数据库级处理选课高峰期的并发问题如果只靠前端限制一定是防不住的。后端必须在事务里做两层校验容量校验 时间冲突校验。容量校验的核心是这个事务逻辑from django.db import transaction from django.db.models import F def enroll_course(request, course_id): course Course.objects.select_for_update().get(idcourse_id) if course.selected_count course.capacity: return {code: 400, message: 课程已满} Enrollment.objects.create(studentrequest.user, coursecourse, statusactive) Course.objects.filter(idcourse_id).update(selected_countF(selected_count) 1) return {code: 200, message: 选课成功}注意select_for_update()会把这一行锁住直到事务结束。如果不用行锁两个学生同时选最后一门课时两个请求都读到selected_count49然后都执行加1最后实际选课人数变成51课程的容量形同虚设。用行锁之后第二个请求必须等第一个事务提交后才能读这行这时候它读到的已经是50就会直接走到课程已满分支。这是我建议每个做选课系统的人都必须注意的点。时间冲突校验不建议放在事务里做复杂的重叠区间判断更简单有效的方案是在选课时查出学生已有的所有有效选课记录取这些课程的time_slot字段然后跟当前课程的time_slot做遍历比较。如果time_slot设计成星期几列表加节次列表比对就是数组求交集几行代码的事。3. 后端接口开发与核心业务实现3.1 用Pycharm创建一个干净的Django项目Pycharm在写Python项目时确实有自己的优势调试模式直观模板补全好用每次写完接口按一下Debug就能看请求上下文。这里是创建一个标准Django项目的流程# 在Pycharm的Terminal里执行 mkvirtualenv exam_system -p python3.10 pip install django djangorestframework corsheaders django-admin startproject exam_system . python manage.py startapp users python manage.py startapp courses python manage.py startapp exams项目目录结构建议按业务拆app不要所有代码都塞在一个app里。拆开后users/app管登录注册权限courses/app管课程和选课exams/app管考试安排和成绩录入。这样后面查问题、改需求定位特别快。创建完app后记得在settings.py里把自己写的app以及rest_framework和corsheaders都注册上具体是放在INSTALLED_APPS列表里。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, corsheaders, users, courses, exams, ]3.2 基于DRF实现登录注册模块含JWT登录注册是所有系统都绕不开的第一关。Django本身有django.contrib.auth但给前端接口用不太方便一般会配合djangorestframework-simplejwt实现JWT登录。安装并配置JWT的关键步骤pip install djangorestframework-simplejwt在settings.py里配置认证类REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], }然后到urls.py中注册登录接口from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns [ path(api/auth/login/, TokenObtainPairView.as_view(), nametoken_obtain_pair), path(api/auth/refresh/, TokenRefreshView.as_view(), nametoken_refresh), ]访问登录接口时前端只需要传username和password就会返回access和refresh两个字段。access是短时有效的令牌一般设为2小时refresh是用来刷新令牌的长时效令牌。前端拿到access之后每次请求在请求头里带上Authorization: Bearer access_token后端就能通过JWT识别出当前请求的用户是谁无需每次查session。这是目前前后端分离项目最主流的认证方案。3.3 选课模块课程列表与选课事务实现课程列表接口比较简单主要是把课程表和教师信息做连表查询。注意要把教师姓名通过related_field拿到不要只返回teacher_id否则前端还得二次请求。from rest_framework import serializers, viewsets from .models import Course class CourseSerializer(serializers.ModelSerializer): teacher_name serializers.CharField(sourceteacher.username, read_onlyTrue) has_enrolled serializers.SerializerMethodField() class Meta: model Course fields [id, name, code, credit, capacity, selected_count, teacher_name, has_enrolled] def get_has_enrolled(self, obj): user self.context[request].user if user.is_authenticated and user.role student: return obj.enrollments.filter(studentuser, statusactive).exists() return False class CourseViewSet(viewsets.ReadOnlyModelViewSet): queryset Course.objects.all() serializer_class CourseSerializer def get_queryset(self): # 只返回当前学期的课程 semester self.request.query_params.get(semester, 2024-2025-1) return self.queryset.filter(semestersemester)has_enrolled这个字段很实用前端用它来控制“选课”按钮变成“退课”按钮学生不需要再额外请求一个已选课程接口来判断状态。选课的操作接口用自定义的action实现from rest_framework.decorators import action from rest_framework.response import Response from django.db import transaction class CourseViewSet(viewsets.ViewSet): action(detailTrue, methods[post]) def enroll(self, request, pkNone): # 事务 行锁防止超选 return Response({message: 选课成功})如果你用的是Flask这套逻辑同样能平移核心区别只是把Django的ORM换成Flask-SQLAlchemy把select_for_update()换成with_for_update()其他思路是一样的。3.4 成绩录入与导出教师端的核心体验成绩录入是教师用得最多的功能体验直接决定这个系统好不好用。教师端至少要有两个接口一是查看某门课程的学生列表及当前成绩二是批量提交成绩。批量提交成绩是典型的“一键导入”场景。Django的bulk_update方法能大幅优化性能但要注意成绩唯一性校验。具体实现逻辑from django.db import transaction def bulk_update_scores(course_id, score_list): with transaction.atomic(): enrollments Enrollment.objects.select_for_update().filter(course_idcourse_id) for score_item in score_list: enrollment enrollments.get(student_idscore_item[student_id]) enrollment.regular_score score_item.get(regular_score) enrollment.final_score score_item.get(final_score) enrollment.score_status submitted Enrollment.objects.bulk_update(enrollments, [regular_score, final_score, score_status])这里的安全点在于Enrollment.objects.select_for_update()锁定了所有选课记录避免了成绩提交过程中有人退课导致数据错乱。分数校验绝对不能只靠前端后端必须做两件事分数范围校验0-100和重复提交校验。我做过一个系统老师连点两次提交后端的接口幂等性没做好批量写入会导致成绩重复翻倍。后来在接口里加了状态判断只有score_statusdraft状态的记录才允许更新问题才彻底解决。考试成绩导出这块用openpyxl库生成xlsx文件再通过HttpResponse返回给前端下载from openpyxl import Workbook from django.http import HttpResponse def export_scores(request, course_id): wb Workbook() ws wb.active ws.title 成绩表 ws.append([学号, 姓名, 平时成绩, 期末成绩, 总分]) for enrollment in Enrollment.objects.filter(course_idcourse_id): ws.append([...]) response HttpResponse(content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet) response[Content-Disposition] attachment; filenamescores.xlsx wb.save(response) return response生成Excel时有个小坑如果文件名是中文浏览器下载时经常乱码。解决办法是给文件名做URL编码from urllib.parse import quote filename quote(数据结构成绩单.xlsx) response[Content-Disposition] fattachment; filename*utf-8\\{filename}4. 前端Vue核心设计与开发实录4.1 用Vue脚手架初始化项目并配置路由前端这层建议直接用Vue3 Vite vue-router pinia这套组合。Vite比webpack的启动速度快太多配置也简单现在基本是主流选项。初始化项目npm create vitelatest web_front -- --template vue cd web_front npm install vue-router4 pinia axios element-pluselement-plus是Vue3对应的组件库表格、表单、弹窗这些都有现成的做管理类系统效率很高。如果你是新手别自己从头画表格样式直接用组件库至少省一个月时间。路由配置要注意权限控制。我的做法是在路由meta里存requiresAuth和roles字段然后在router.beforeEach里检查登录状态和角色。import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(../views/LoginView.vue) }, { path: /courses, component: () import(../views/CourseListView.vue), meta: { requiresAuth: true, roles: [student] } }, { path: /admin/scores, component: () import(../views/AdminScoreView.vue), meta: { requiresAuth: true, roles: [admin, teacher] } } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(access_token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })4.2 封装axios请求器与动态权限菜单axios请求器的封装是一切请求调试的地基。把所有通用逻辑集中在一个文件里后续根本不用每个页面重复写token处理。// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(access_token) router.push(/login) ElMessage.error(登录已过期请重新登录) } return Promise.reject(error) } ) export default request动态权限菜单的思路是登录成功后后端根据当前用户的角色返回菜单列表前端拿到菜单列表之后动态添加路由。这个过程用router.addRoute来实现代码量不大但很能体现你对项目的理解。毕业答辩时把这个讲透评委会觉得你的系统提供了登录之后的权限设计方案。4.3 选课业务的前端核心页面实现选课页面是整个系统前端里交互逻辑最复杂的一个。学生需要看到课程列表含余量/已选状态、已选课程、退课操作、选课时间窗口倒计时提示。部分关键代码template el-table :datacourses stripe el-table-column propname label课程名称 / el-table-column propcredit label学分 / el-table-column propteacher_name label授课教师 / el-table-column label剩余名额 template #defaultscope span{{ scope.row.capacity - scope.row.selected_count }}/span /template /el-table-column el-table-column label操作 template #defaultscope el-button v-if!scope.row.has_enrolled typeprimary clickhandleEnroll(scope.row) :disabled!canEnroll 选课/el-button el-button v-else typedanger clickhandleDrop(scope.row) 退课/el-button /template /el-table-column /el-table /template script setup import { ref, onMounted } from vue import request from ../utils/request const courses ref([]) const canEnroll ref(true) async function fetchCourses() { const res await request.get(/courses/) courses.value res.data } async function handleEnroll(course) { const res await request.post(/courses/${course.id}/enroll/) if (res.code 200) { ElMessage.success(res.message) fetchCourses() } } /script写完选课页之后我强烈建议你测一下这个场景选课时网络卡顿前端发出请求后迟迟没有响应学生着急又点了一次。如果没有做按钮的loading状态就会发出两次请求。项目答辩的时候老师特别喜欢看这种细节。处理办法很简单用loading变量控制按钮状态el-button :loadingenrollingId scope.row.id clickhandleEnroll(scope.row)选课/el-button4.4 成绩可视化与图表展示成绩系统的前端不能光做几个表格交差加上可视化图表会让整个项目提高一个档次。Element Plus自身的表格够用但图表需要用到echarts。安装方式npm install echarts --save实现一个简单的成绩分布柱状图template div refchartRef styleheight: 350px/div /template script setup import * as echarts from echarts import { ref, onMounted, watch } from vue import request from ../utils/request const chartRef ref(null) async function renderChart(courseId) { const res await request.get(/courses/${courseId}/score-distribution/) const chart echarts.init(chartRef.value) chart.setOption({ title: { text: 成绩分布分段统计 }, xAxis: { type: category, data: [0-59, 60-69, 70-79, 80-89, 90-100] }, yAxis: { type: value }, series: [{ type: bar, data: res.data, itemStyle: { color: #409EFF } }] }) } /script成绩分布图的接口在后端实现很简单用Django的Count函数配合Case表达式把所有成绩按分数段分组统计from django.db.models import Count, Case, When, IntegerField stats Enrollment.objects.filter(course_idcourse_id, score_statussubmitted).aggregate( failCount(Case(When(avg_score__lt60, then1), output_fieldIntegerField())), pass_60Count(Case(When(avg_score__gte60, avg_score__lt70, then1), output_fieldIntegerField())), # ... 其他分数段 )有了图表之后整个系统的“考试成绩管理”才真正具有了可看性——不只是一堆数据躺在表格里而是能直观看到优秀率、及格率等关键教学指标。这部分在答辩时也是一个加分项。5. 环境配置与部署环节的避坑指南5.1 Pycharm中配置Python解释器与依赖管理Pycharm环境配置是新手最容易卡住的地方。很多人项目代码没错结果跑不起来问题出在解释器选错了。Pycharm右下角可以看到当前解释器务必确认选的是你的虚拟环境而不是全局的Python。建议在项目根目录维护一个requirements.txt把依赖一次性固定下来Django4.2.7 djangorestframework3.14.0 djangorestframework-simplejwt5.3.0 django-cors-headers4.3.1 openpyxl3.1.2以后换电脑、给老师跑演示直接pip install -r requirements.txt能省出大把时间。Pycharm还自带一个很实用的功能在Terminal底部面板里可以直接看到虚拟环境前缀类似(venv)如果没看到这个前缀说明解释器没配对。5.2 跨域配置前后端端口不一致怎么处理开发阶段前端Vite默认跑在5173端口后端Django/Flask跑在8000端口两个端口不一样就一定会遇到跨域问题。配置CORS的办法在Django里是这样的# settings.py INSTALLED_APPS [ corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ] CORS_ALLOW_ALL_ORIGINS True # 开发阶段先全部放开Flask那边稍微简单一点用flask-cors库from flask_cors import CORS CORS(app)跨域配置放开到CORS_ALLOW_ALL_ORIGINS True只适用于开发阶段。如果项目要部署上线建议改为指定域名白名单CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ]这里有个很常见的坑前端请求能通但带不了cookies或自定义请求头。如果你的JWT是通过请求头Authorization传递的话默认情况没问题但如果有跨域请求附带自定义头需要在后端加入允许的请求头配置CORS_ALLOW_HEADERS [ content-type, authorization, ]5.3 Flask版本的核心接口写法对照虽然我主推Django但既然题目里也提到了Flask就把核心接口的Flask版本写出来方便对比选择。Flask项目结构一般是这样exam_system_flask/ ├── app.py ├── models.py ├── extend.py └── requirements.txt核心接口示例from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import JWTManager, jwt_required, get_jwt_identity app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///exam.db app.config[JWT_SECRET_KEY] your-secret-key db SQLAlchemy(app) jwt JWTManager(app) app.route(/api/courses, methods[GET]) jwt_required() def list_courses(): courses Course.query.all() return jsonify([c.to_dict() for c in courses]) app.route(/api/courses/int:course_id/enroll, methods[POST]) jwt_required() def enroll_course(course_id): user_id get_jwt_identity() # 容量校验 唯一约束处理 # ... return jsonify({code: 200, message: 选课成功}) if __name__ __main__: app.run(debugTrue, port8000)Flask的自主性更强目录结构可以完全自己掌控Django则自带了很多约束和规范。我个人建议新手直接学Django因为Django的约束会帮你规避很多低级错误比如默认帮你处理了SQL注入、CSRF等问题。5.4 部署上线的最小可行方案如果项目需要用腾讯云、阿里云的服务器做演示部署方案有很多但毕业设计场景最简单可靠的是后端用Gunicorn起服务前端打包成静态文件交给Nginx托管。Django部署的关键步骤pip install gunicorn # 收集静态文件 python manage.py collectstatic # 迁移数据库到生产环境 python manage.py migrate # 启动Gunicorn服务 gunicorn exam_system.wsgi:application --bind 0.0.0.0:8000 --workers 3前端Vue项目打包npm run build # 会在dist目录生成静态文件将整个dist目录上传到服务器Nginx配置里最关键的一处是把API请求反代到后端server { listen 80; server_name your_domain.com; root /var/www/exam_front; index index.html; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }最后那句try_files $uri $uri/ /index.html;特别重要。Vue是单页应用路由跳转后页面刷新会直接404需要靠它把请求全部引回index.html再让Vue路由接管。6. 真实开发中常见的五个大坑与排查思路6.1 选课并发写库导致数据错乱这个坑我上面提过但值得单独再讲一下。我第一版做选课系统时没有用行锁上线当天就出问题了。不止是超选问题更隐蔽的是selected_count字段在并发下变成负数。排查这类问题的最快方法直接看MySQL慢查询日志或者在后端加日志记录请求的耗时和结果。如果发现两个选课请求几乎同时进入就基本可以断定是事务隔离等级和锁的机制没处理好。解决办法就是select_for_update()加事务确实有效。6.2 Django的DateTimeField时区问题这个坑出现频率极高本地开发时时间显示正常部署到服务器后发现所有时间都差了8小时。原因在于Django默认时区是UTC而中国是东八区。解决办法在settings.py中TIME_ZONE Asia/Shanghai USE_TZ True如果数据库里已经存了错误的时间数据恢复起来比较麻烦。我自己踩过的泥坑是项目开始时就该写好时区配置数据库里的旧数据如果不是用Django ORM写入的修复时手工加8小时核对就行。6.3 Vue中m3u8视频播放问题有的学校会把课程录像放到系统里让学生看有些场景会遇到播放m3u8视频的需求主要是在线播放教学视频。严格说这不是教务选课系统的核心模块但如果老师提了要求就得能处理。m3u8是HLS流媒体协议的视频分段索引文件普通video标签默认不支持直接播放。解决方案是用hls.js库同时配合后端把视频文件通过静态路由提供。在Vue项目里安装npm install hls.js前端播放组件的核心逻辑是判断用户浏览器是否原生支持HLSSafari原生支持不支持的话就用hls.js接管import Hls from hls.js function playVideo(videoElement, videoUrl) { if (videoElement.canPlayType(application/vnd.apple.mpegurl)) { // Safari等原生支持HLS的浏览器 videoElement.src videoUrl } else if (Hls.isSupported()) { // 其他浏览器用hls.js解析 const hls new Hls() hls.loadSource(videoUrl) hls.attachMedia(videoElement) } else { console.error(当前浏览器不支持HLS播放) } }注意的竟然是后端。后端返回m3u8和ts分片时响应头的Content-Type必须正确m3u8文件要返回application/vnd.apple.mpegurlts分片要返回video/mp2t。Django的静态文件服务默认可能处理不了建议用一个简单的视图函数来响应请求。6.4 前后端联调时接口404的排查方法联调阶段最常遇到的问题就是后端接口明明存在但前端访问却404。排查思路一定是从内到外用curl或Postman直接请求后端接口看是否正常。确认后端服务是否在运行、端口是否正确。检查urls.py中的路由是否有层级错误。检查前端是否配置了正确的代理地址。如果用的是Vite开发环境推荐在vite.config.js里配置开发代理把/api请求转发到后端能避免大部分跨域问题export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true } } } })配置这个代理之后前端请求/api/courses/时实际上会被转发到http://localhost:8000/api/courses/解决了开发环境的跨域问题。6.5 Flask sqlite生产环境并发差的问题用Flask开发时为了方便默认用的往往是SQLite数据库。单机开发没问题但部署到服务器、同时处理多个选课请求时SQLite的写锁问题就会暴露出来表现为高并发时接口超时甚至卡死。解决方案两个一是换用MySQL基本一劳永逸二是如果项目很简单不想引入MySQL至少把SQLite的隔离级别和同步模式调整一下。但我的建议是既然都已经做毕业设计或正式项目了直接用MySQL就好安装不复杂而且能顺带熟悉生产环境配置。Windows环境装MySQL之后Flask的数据库URI改成app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:yourpasslocalhost/exam_system?charsetutf8mb4注意charsetutf8mb4这个参数如果用默认的utf8存emoji或特殊符号会报错。7. 一套可直接使用的开发顺序与时间排期7.1 从零开始的两个月节奏规划做教务系统这类项目最大的坑是需求蔓延。一开始觉得功能很简单做着做着发现“老师想把整个OA都搬进来”结果两年都做不完。我给一个能在一两个月内完成、并真正达到“学校教务系统”核心能力的功能集合第一阶段第1周需求确认 数据库设计。把所有核心表建好把业务理清楚。这阶段不写业务代码只写模型定义和数据库迁移。第二阶段第2-4周后端核心接口开发。先做登录注册和用户权限再做课程管理和选课接口最后做成绩录入与统计。第三阶段第5-6周前端页面开发。按功能拆页面登录页、课程列表页、选课操作页、我的课表页、成绩查询页、后台管理页。第四阶段第7周前后端联调。这个阶段主要是改bug接口参数对不上、状态码处理不一致、边界情况没考虑都会在这里集中暴露。第五阶段第8周部署演示环境 写文档。别小看这步答辩时不光看系统功能还看文档和演示流畅度。7.2 开发时推荐遵循的敏捷节奏很多独自开发的同学有个误区憋大招猛写两周代码然后一次性测试。结果往往是一堆bug堆积在一起根本没法排。我的习惯是每天都要保证系统处于可运行状态早上确定今天要完成的一个功能点比如“完成选课接口”。写完后立刻起服务从前端页面点一遍确认正常。下班前用git提交一次commit message写清楚“完成选课事务处理逻辑”。这样每周末都能给老师演示一个阶段版本过程是平滑的、可持续的。如果哪次做砸了就是哪天的事定位很容易不用从一大团乱麻里找bug。7.3 Flask与Django的取舍补充最后再补充一点关于框架选型的个人经验。如果你问我现在给自己做新项目选Django还是Flask我的答案很明确业务复杂、模块多、需要后台管理的选Django工具型网页、三五天出活的项目用Flask完全可以。但两者在教务选课这种场景里都能胜任区别主要是开发者的熟悉度和生态依赖。还有一个新东西值得留意FastAPI现在越来越流行尤其适合做纯API服务。如果你不打算用Django的自带admin和模板系统又想体验新版本Python异步特性FastAPI SQLAlchemy Vue也是一条走得通的路。但毕业设计和入门项目的话资料最多的还是Django这也是我把主要篇幅放在Django上的原因。最后分享一点个人感悟做了快十年的开发我见过太多类似项目半路烂尾的情况。不是说技术难而是需求不明确、没坚持、代码越写越乱。教务选课考试系统这个题目其实很适合作为综合练习它包含用户权限、事务并发、前端组件、数据可视化、文件导出、部署上线几乎把一个完整Web系统能遇到的问题覆盖了个遍。你做完这个项目之后简历上有东西写了面试能聊的内容也丰富了。以后换其他系统核心思路完全可迁移。比如把课程换成商品就是电商系统把考试成绩换成订单就是订单管理系统。选课系统做的不是“选课”是业务建模能力和工程实操能力。如果你正在做类似系统遇到问题卡住了回头看看这篇里的几个大坑选课并发记得加行锁、时区记得改成上海、跨域不要放太开、前端按钮注意防重复点击、SQLite别用在生产环境。把这些坑避开一个合格的教务系统就成功了一半。剩下的一半就是坐下来一行一行写代码而已。