1. 项目概述与设计思路
作为一名经历过三次选课系统崩溃的老学长,我深知一个稳定可靠的选课系统对学生有多重要。这次我们用Flask+Vue.js技术栈,从零构建一个轻量级但功能完备的网上选课系统。不同于传统教务系统,我们的设计重点在于:
- 高并发处理:采用Flask异步扩展应对选课高峰
- 实时交互:Vue.js前端实现无刷新操作体验
- 防超卖机制:数据库事务+乐观锁保证选课数据一致性
- 权限隔离:JWT细粒度控制学生/教师/管理员权限
技术选型上,Flask的轻量特性让我们可以快速迭代,而Vue的响应式开发则完美适配选课这类表单密集场景。PyCharm作为主力IDE,其专业版对Vue模板的支持能显著提升开发效率。
2. 开发环境配置
2.1 后端环境搭建
推荐使用PyCharm Professional 2023+(社区版需手动配置Vue支持):
bash复制# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate.bat # Windows
# 安装核心依赖
pip install flask flask-restful flask-sqlalchemy flask-jwt-extended flask-cors
注意:务必使用Python 3.8+版本,低版本可能遇到异步支持问题
2.2 前端环境准备
VSCode或WebStorm均可,但个人更推荐VSCode+Volar扩展:
bash复制npm init vue@latest course-selection-frontend
cd course-selection-frontend
npm install axios element-plus vue-router pinia
配置vite.config.js解决开发环境跨域:
javascript复制server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
3. 数据库设计精要
3.1 核心表结构
使用SQLAlchemy定义模型时特别注意约束条件:
python复制class Enrollment(db.Model):
__tablename__ = 'enrollment'
id = db.Column(db.Integer, primary_key=True)
student_id = db.Column(db.Integer, db.ForeignKey('student.id'))
course_id = db.Column(db.Integer, db.ForeignKey('course.id'))
select_time = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = (
# 防止重复选课
db.UniqueConstraint('student_id', 'course_id', name='uix_student_course'),
# 添加索引提升查询性能
db.Index('idx_student_course', 'student_id', 'course_id'),
)
class Course(db.Model):
# ...
remaining = db.Column(db.Integer) # 实时剩余名额
version_id = db.Column(db.Integer) # 乐观锁版本号
3.2 关键业务逻辑实现
选课接口需要特别注意并发控制:
python复制@app.route('/api/enroll', methods=['POST'])
@jwt_required()
def enroll_course():
student_id = get_jwt_identity()
course_id = request.json.get('course_id')
try:
# 开启数据库事务
db.session.begin_nested()
course = Course.query.filter_by(id=course_id).with_for_update().first()
if not course or course.remaining <= 0:
raise ValueError("课程已满")
# 检查是否已选
exists = Enrollment.query.filter_by(
student_id=student_id,
course_id=course_id
).first()
if exists:
raise ValueError("不可重复选课")
# 更新名额
course.remaining -= 1
new_enroll = Enrollment(student_id=student_id, course_id=course_id)
db.session.add(new_enroll)
db.session.commit()
return jsonify({"msg": "选课成功"}), 200
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 400
4. 前端关键实现
4.1 课程列表页优化
使用Vue3组合式API实现带防抖的筛选:
vue复制<script setup>
import { ref, computed } from 'vue'
const searchText = ref('')
const courses = ref([])
// 防抖函数
const debounce = (fn, delay) => {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
// 带筛选的计算属性
const filteredCourses = computed(() => {
return courses.value.filter(c =>
c.name.includes(searchText.value) ||
c.teacher.includes(searchText.value)
)
})
// 获取课程数据
const fetchCourses = debounce(async () => {
try {
const res = await axios.get('/api/courses')
courses.value = res.data
} catch (err) {
console.error('获取课程失败:', err)
}
}, 300)
</script>
4.2 选课操作交互设计
Element Plus的ElMessage组件配合状态管理:
vue复制<template>
<el-button
type="primary"
:disabled="course.remaining <= 0"
@click="handleEnroll(course.id)"
>
{{ course.remaining > 0 ? '立即选课' : '已满员' }}
</el-button>
</template>
<script setup>
import { useStore } from '@/stores/course'
const store = useStore()
const handleEnroll = async (courseId) => {
try {
await axios.post('/api/enroll', { course_id: courseId })
ElMessage.success('选课成功')
store.refreshCourses() // 更新全局状态
} catch (err) {
ElMessage.error(err.response?.data?.error || '选课失败')
}
}
</script>
5. 部署实战方案
5.1 生产环境配置
推荐使用Docker Compose编排服务:
dockerfile复制# backend/Dockerfile
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-w 4", "-k gevent", "--bind 0.0.0.0:5000", "app:app"]
dockerfile复制# frontend/Dockerfile
FROM node:16 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
5.2 Nginx关键配置
nginx复制server {
listen 80;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 静态资源缓存
location /assets {
expires 1y;
add_header Cache-Control "public";
}
}
6. 踩坑实录与性能优化
6.1 高频问题排查
-
跨域问题:
- 开发环境:确保Vite代理配置正确
- 生产环境:Nginx需正确处理OPTIONS请求
- Flask需配置CORS(app, resources={r"/api/": {"origins": ""}})
-
选课超卖:
- 使用SELECT FOR UPDATE悲观锁
- 或采用version字段实现乐观锁
- 最终方案:Redis分布式锁+数据库事务
-
JWT失效:
- 注意时区问题导致token提前失效
- 前端需处理401自动跳转登录页
- 推荐使用axios拦截器统一处理
6.2 性能优化技巧
-
数据库层面:
python复制# 使用joinedload避免N+1查询 courses = Course.query.options( joinedload(Course.teacher), joinedload(Course.enrollments) ).all() -
缓存策略:
python复制from flask_caching import Cache cache = Cache(config={'CACHE_TYPE': 'Redis'}) @app.route('/api/courses') @cache.cached(timeout=60) def get_courses(): # ... -
前端性能:
- 使用Vue的keep-alive缓存页面
- 路由懒加载component: () => import('./views/Courses.vue')
- 图片资源使用WebP格式
7. 扩展功能实现
7.1 课程冲突检测
在后端添加校验逻辑:
python复制def check_schedule_conflict(student_id, new_course_time):
enrolled_courses = Course.query.join(Enrollment).filter(
Enrollment.student_id == student_id
).all()
for course in enrolled_courses:
if time_overlap(course.schedule, new_course_time):
raise ValueError("时间冲突")
7.2 排队系统设计
使用Redis实现选课队列:
python复制import redis
r = redis.Redis()
@app.route('/api/enroll', methods=['POST'])
def enroll_course():
student_id = get_jwt_identity()
course_id = request.json.get('course_id')
# 加入队列
queue_key = f"course_queue:{course_id}"
position = r.rpush(queue_key, student_id)
# 获取当前队列位置
current_pos = r.lpos(queue_key, student_id)
return jsonify({
"queue_position": current_pos + 1,
"total_in_queue": r.llen(queue_key)
}), 202
这个选课系统从第一行代码到最终部署,我完整走过了三次迭代。最大的体会是:看似简单的业务逻辑(如选课)在高并发场景下会暴露出各种边界问题。建议在开发初期就做好压力测试,使用Locust等工具模拟真实选课场景。