ARTICLE DETAIL

资讯详情

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

Python课程设计:选课系统完整实现与避坑指南

Python课程设计:选课系统完整实现与避坑指南 简介这份资源是面向高校 Python 程序设计课程学习者的课程大作业完整源码包以「课程选课系统」为实战题目适合正在准备期末项目、需要参考分层架构与面向对象设计的学生。项目围绕学校、学员、课程、讲师、管理员五类角色展开要求创建武汉、长沙两所学校并在不同校区开设 linux、python、C 语言课程涵盖课程周期与价格设置、讲师创建、学员注册并关联班级等业务同时提供学员视图、讲师视图和管理视图三套接口数据通过 pickle 序列化持久化保存。压缩包共 53 个文件约 759KB以 13 个 py 源码文件为核心辅以 12 个 pyc 编译文件、18 张 png 运行截图以及 docx 作业要求、md 说明文档和若干数据文件完整呈现了从需求到实现的开发过程。目前已有 52 人学习读者可借此理解模块划分、接口层与核心层分离的设计思路并对照截图快速复现选课、成绩修改等关键功能。1. 课程选课系统一个被低估的 Python 大作业选题每年期末总有一批人卡在课程设计上。选题太简单答辩时被追问两句就露馅选题太复杂代码写到一半自己先崩了。课程选课系统恰好卡在中间那个甜点区——业务逻辑足够完整能撑起一个像样的程序设计大作业技术栈又足够收敛用 Python 标准库加一个轻量 Web 框架就能跑通全流程。这个选题的核心价值在于它逼着你把「学生、课程、选课记录」三张表之间的关系理清楚。选课冲突怎么判、容量满了怎么处理、退课之后名额怎么释放这些不是背语法能解决的问题而是真实的数据建模和状态管理。对于正在学 Python、需要交一份能拿得出手的课程设计的人来说选课系统是一个能让你在答辩时讲出东西的题目。下面我从环境搭建开始一步步把这个系统拆开讲透。2. 环境准备与项目结构从零把选课系统跑起来2.1 Python 安装与虚拟环境配置很多人拿到一份 Python 源码压缩包第一步就卡住了——双击 .py 文件没反应或者在命令行里敲 python 提示找不到命令。这不是代码的问题是环境没配好。Windows 上安装 Python去官网下载安装包时务必勾选「Add Python to PATH」这个选项不勾后面命令行里调用 python 和 pip 都会报错。安装完成后打开终端验证python --version pip --version两条命令都能正常输出版本号说明基础环境没问题。如果 python 命令不识别但 python3 可以说明系统里同时存在多个版本后续统一用 python3 和 pip3 即可。接下来是虚拟环境。课程设计项目不建议直接装在全局环境里依赖冲突是血泪教训。在项目根目录下执行python -m venv venvWindows 激活命令是venv\Scripts\activatemacOS 和 Linux 是source venv/bin/activate。激活后终端提示符前面会出现(venv)标记之后所有 pip 安装都只影响这个虚拟环境。提示如果 PowerShell 下激活报「无法加载文件因为在此系统上禁止运行脚本」以管理员身份打开 PowerShell 执行Set-ExecutionPolicy RemoteSigned即可。2.2 依赖安装与目录结构解读选课系统常见的依赖组合是 Flask SQLite。Flask 轻量、文档多、报错信息友好SQLite 不需要额外安装数据库服务一个文件就是整个数据库非常适合课程设计场景。在激活的虚拟环境中执行pip install flask flask-sqlalchemy安装完成后用pip list确认这两个包出现在列表里。如果下载速度慢可以临时指定国内镜像源但不要写死在配置文件里换一台机器可能就失效了。一个典型的选课系统源码目录结构大致如下course_selection/ ├── app.py # 应用入口路由和视图函数 ├── models.py # 数据模型定义 ├── templates/ # HTML 模板 │ ├── login.html │ ├── course_list.html │ └── my_courses.html ├── static/ # CSS、JS 等静态资源 ├── requirements.txt # 依赖清单 └── data.db # SQLite 数据库文件首次运行自动生成拿到源码后先看requirements.txt用pip install -r requirements.txt一次性装齐依赖。然后看app.py最底部有没有if __name__ __main__:这样的启动入口有的话直接python app.py就能跑起来。浏览器访问http://127.0.0.1:5000看到登录页面说明项目骨架是通的。2.3 数据库初始化与首次运行SQLite 数据库文件通常不会随源码一起打包需要首次运行时自动创建。在app.py里一般会有类似这样的初始化逻辑from flask import Flask from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///data.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db SQLAlchemy(app) # 首次运行时创建所有表 with app.app_context(): db.create_all()SQLALCHEMY_DATABASE_URI里的sqlite:///data.db表示数据库文件生成在项目实例目录下。db.create_all()只会创建尚不存在的表已存在的表不会被覆盖所以重复运行不会丢数据。如果运行时报「no such table」错误说明建表逻辑没执行到。检查db.create_all()是否在app.app_context()上下文里调用这是 Flask-SQLAlchemy 的硬性要求。另一个常见问题是数据库文件路径不对SQLite 找不到文件时会静默创建一个空库导致表结构缺失。可以在初始化后打印一下app.instance_path确认实际路径。3. 数据模型设计三张表撑起整个选课逻辑3.1 学生表、课程表与选课记录表的关系选课系统的数据模型不复杂但关系必须理清楚。核心是三张表学生表存账号信息课程表存课程信息和容量选课记录表存「谁选了什么课」这个多对多关系。from datetime import datetime class Student(db.Model): id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse) password db.Column(db.String(100), nullableFalse) name db.Column(db.String(50)) enrollments db.relationship(Enrollment, backrefstudent, lazyTrue) class Course(db.Model): id db.Column(db.Integer, primary_keyTrue) code db.Column(db.String(20), uniqueTrue, nullableFalse) name db.Column(db.String(100), nullableFalse) capacity db.Column(db.Integer, default50) enrolled_count db.Column(db.Integer, default0) enrollments db.relationship(Enrollment, backrefcourse, lazyTrue) class Enrollment(db.Model): id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(student.id), nullableFalse) course_id db.Column(db.Integer, db.ForeignKey(course.id), nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow) __table_args__ (db.UniqueConstraint(student_id, course_id, nameunique_enrollment),)学生表和课程表各自持有enrollments关系属性通过backref可以反向查询。选课记录表上的UniqueConstraint是关键——它在数据库层面保证同一个学生不能重复选同一门课比在应用层用 if 判断可靠得多。enrolled_count字段是一个冗余设计用来快速判断课程是否已满。每次选课成功时加一退课时减一。这样做的好处是查询课程列表时不需要实时 count 选课记录性能更好。代价是必须保证这个计数和实际记录数一致后面会讲怎么处理不一致的情况。3.2 选课冲突检测的字段设计「冲突检测」是选课系统里最容易被答辩老师追问的点。最基础的冲突是时间冲突——同一时间段只能选一门课。这需要在课程表里加上时间字段class Course(db.Model): # ... 其他字段同上 day_of_week db.Column(db.Integer) # 1-7 表示周一到周日 start_period db.Column(db.Integer) # 第几节开始 end_period db.Column(db.Integer) # 第几节结束判断两门课是否冲突的逻辑是同一天且时间段有重叠。重叠的条件是start1 end2 and start2 end1。这个判断放在选课视图函数里选课前先查出该学生已选的所有课程逐一比对。另一种冲突是容量冲突这个更简单——选课前检查course.enrolled_count course.capacity。但要注意并发问题两个学生同时选最后一门课的最后一个名额时如果先查后改可能都查到「未满」然后都写入。解决办法是在更新计数时用数据库层面的原子操作或者给课程行加锁。课程设计规模小用 SQLite 的事务加上UPDATE ... WHERE enrolled_count capacity这种条件更新就能避免超选。3.3 用 Flask-SQLAlchemy 建表并灌入测试数据模型定义好之后写一个初始化脚本灌入测试数据方便开发和演示def init_data(): with app.app_context(): db.create_all() if Student.query.first(): return # 已有数据则跳过 s1 Student(usernamestu001, password123456, name张三) s2 Student(usernamestu002, password123456, name李四) c1 Course(codeCS101, namePython程序设计, capacity2, day_of_week1, start_period1, end_period2) c2 Course(codeCS102, name数据结构, capacity50, day_of_week1, start_period3, end_period4) db.session.add_all([s1, s2, c1, c2]) db.session.commit() print(测试数据初始化完成)注意c1的容量故意设为 2方便测试「课程已满」的场景。day_of_week和start_period的设置让CS101和CS102不冲突但如果再加一门周一第一节的课就会触发时间冲突检测。运行这个脚本后用sqlite3 data.db进入数据库执行.tables应该能看到student、course、enrollment三张表。SELECT * FROM course;能看到灌入的课程数据。这一步确认无误后再写业务逻辑否则后面出问题很难判断是模型错了还是逻辑错了。4. 核心功能实现选课、退课与冲突判断4.1 选课接口的完整逻辑与事务处理选课是整个系统最核心的操作涉及多个检查步骤和一次数据写入。完整的选课视图函数如下from flask import request, jsonify, session app.route(/api/enroll, methods[POST]) def enroll(): student_id session.get(student_id) if not student_id: return jsonify({code: 401, msg: 请先登录}), 401 course_id request.json.get(course_id) course Course.query.get(course_id) if not course: return jsonify({code: 404, msg: 课程不存在}), 404 # 检查是否已选 exist Enrollment.query.filter_by( student_idstudent_id, course_idcourse_id).first() if exist: return jsonify({code: 400, msg: 已选过该课程}), 400 # 检查容量 if course.enrolled_count course.capacity: return jsonify({code: 400, msg: 课程已满}), 400 # 检查时间冲突 my_courses Course.query.join(Enrollment).filter( Enrollment.student_id student_id).all() for c in my_courses: if c.day_of_week course.day_of_week: if c.start_period course.end_period and course.start_period c.end_period: return jsonify({code: 400, msg: f与{c.name}时间冲突}), 400 # 写入选课记录并更新计数 try: enrollment Enrollment(student_idstudent_id, course_idcourse_id) course.enrolled_count 1 db.session.add(enrollment) db.session.commit() except Exception as e: db.session.rollback() return jsonify({code: 500, msg: 选课失败请重试}), 500 return jsonify({code: 0, msg: 选课成功})这段代码的执行顺序是登录校验 → 课程存在性校验 → 重复选课校验 → 容量校验 → 时间冲突校验 → 写入。每一步失败都提前返回不继续往下走。db.session.commit()放在 try 块里失败时rollback()回滚。选课记录写入和计数更新在同一个事务里要么都成功要么都失败不会出现「记录写了但计数没加」的中间状态。注意course.enrolled_count 1这种写法在并发下不是原子的。课程设计场景并发量极低通常不会出问题。如果答辩时被问到可以回答「生产环境应该用UPDATE course SET enrolled_count enrolled_count 1 WHERE id ? AND enrolled_count capacity这种条件更新来保证原子性」。4.2 退课逻辑与名额释放退课比选课简单但有一个容易忽略的点——退课后必须把enrolled_count减回去否则名额会越来越少最后所有课都显示已满。app.route(/api/drop, methods[POST]) def drop(): student_id session.get(student_id) course_id request.json.get(course_id) enrollment Enrollment.query.filter_by( student_idstudent_id, course_idcourse_id).first() if not enrollment: return jsonify({code: 400, msg: 未选该课程}), 400 course Course.query.get(course_id) try: db.session.delete(enrollment) if course.enrolled_count 0: course.enrolled_count - 1 db.session.commit() except Exception: db.session.rollback() return jsonify({code: 500, msg: 退课失败}), 500 return jsonify({code: 0, msg: 退课成功})if course.enrolled_count 0这个判断是防御性的。正常情况下计数不会小于零但如果之前有过手动改数据库或者异常操作导致计数不对这个判断能防止出现负数。退课不需要检查时间冲突和容量因为退课只会释放资源不会占用新资源。但需要确认这条选课记录确实存在否则删除一个不存在的记录会静默成功返回「退课成功」但实际什么都没发生。4.3 课程列表查询与已选状态标记学生登录后看到的课程列表需要标记出哪些已经选过、哪些已满、哪些与已选课程冲突。这个查询逻辑放在视图函数里app.route(/api/courses) def course_list(): student_id session.get(student_id) courses Course.query.all() my_course_ids set() my_courses [] if student_id: my_enrollments Enrollment.query.filter_by(student_idstudent_id).all() my_course_ids {e.course_id for e in my_enrollments} my_courses Course.query.filter(Course.id.in_(my_course_ids)).all() result [] for c in courses: item { id: c.id, code: c.code, name: c.name, capacity: c.capacity, enrolled: c.enrolled_count, day_of_week: c.day_of_week, start_period: c.start_period, end_period: c.end_period, selected: c.id in my_course_ids, full: c.enrolled_count c.capacity, conflict: False } # 标记时间冲突 if not item[selected]: for mc in my_courses: if mc.day_of_week c.day_of_week: if mc.start_period c.end_period and c.start_period mc.end_period: item[conflict] True break result.append(item) return jsonify({code: 0, data: result})返回的 JSON 里每个课程带有selected、full、conflict三个布尔标记前端根据这些标记决定按钮是显示「选课」还是「已选」「已满」「冲突」。把判断逻辑放在后端前端只负责展示这样即使前端被绕过直接调接口后端仍然会做完整校验。my_course_ids用 set 而不是 list因为后面要做in判断set 的查找是 O(1)list 是 O(n)。课程数量少的时候差别不大但这是一个好习惯。5. 避坑与排查选课系统开发中最容易翻车的五个地方5.1 数据库文件路径不对导致表建不出来现象运行python app.py没有报错但访问页面时提示「no such table: student」。原因Flask-SQLAlchemy 的 SQLite 路径是相对于app.instance_path的不是相对于项目根目录。如果db.create_all()在错误的上下文里执行或者数据库文件被创建到了其他目录就会出现这个问题。解决在初始化后打印app.instance_path确认实际路径用绝对路径配置SQLALCHEMY_DATABASE_URI例如sqlite:////absolute/path/to/data.db注意四个斜杠。或者在项目根目录下执行find . -name *.db找到数据库文件的实际位置。5.2 选课计数与实际记录数不一致现象课程列表显示已满但实际选课人数没那么多或者显示未满但选课时提示已满。原因enrolled_count是冗余字段如果在某些操作路径中只写了选课记录但没有更新计数或者只更新了计数但没有写记录就会不一致。常见于退课后忘记减计数或者异常回滚不完整。解决写一个修复脚本以enrollment表的实际记录数为准重新计算每门课的enrolled_countdef fix_count(): with app.app_context(): for course in Course.query.all(): actual Enrollment.query.filter_by(course_idcourse.id).count() if course.enrolled_count ! actual: print(f修复 {course.name}: {course.enrolled_count} - {actual}) course.enrolled_count actual db.session.commit()这个脚本可以在每次部署后跑一次也可以做成管理后台的一个按钮。5.3 时间冲突判断的边界条件写反现象明明两门课时间不冲突系统却提示冲突或者真正冲突的课反而能选上。原因时间重叠的判断条件写错了。正确的条件是start1 end2 and start2 end1两个条件必须同时满足。很多人会写成start1 start2 and end1 end2这只覆盖了包含关系漏掉了部分重叠的情况。解决用具体数字验证。假设课程 A 是第 1-2 节课程 B 是第 2-3 节它们在第 2 节重叠应该判为冲突。代入公式1 3 and 2 2结果为 True正确。如果写成1 2 and 2 3结果为 False就漏判了。5.4 虚拟环境没激活就装依赖现象pip install flask显示安装成功但运行代码时提示ModuleNotFoundError: No module named flask。原因系统里有多个 Python 环境pip 装到了全局环境或者另一个虚拟环境里而运行代码用的是当前虚拟环境的解释器。解决安装依赖前先确认终端提示符有(venv)标记。如果没有先激活虚拟环境。安装后用pip list确认包在列表里再用python -c import flask; print(flask.__version__)确认当前解释器能导入。如果还是不行用which python和which pip检查两者是否指向同一个环境。5.5 前端表单提交后页面不跳转现象点击「选课」按钮后接口返回了成功但页面没有任何变化。原因前端用fetch或XMLHttpRequest发请求时没有在回调里处理成功后的页面更新。接口返回的 JSON 数据被丢弃了页面还是旧的。解决在fetch的.then()里根据返回的code判断成功与否成功时刷新课程列表或者直接更新对应行的按钮状态。如果用的是表单直接提交确保后端返回的是重定向而不是 JSON。课程设计里两种方式都可以但前后端分离的写法更清晰也更容易在答辩时展示接口设计能力。6. 从能跑到好用三个让答辩加分的细节6.1 用事务保证选课操作的原子性前面选课接口里已经用了try/commit/rollback的结构但有一个细节可以进一步优化——把容量检查也放进事务里。目前的写法是先查course.enrolled_count再更新两步之间如果有另一个请求修改了计数就可能超选。更严谨的做法是用条件更新from sqlalchemy import update def enroll_atomic(student_id, course_id): with db.session.begin_nested(): # 条件更新只有未满时才增加计数 result db.session.execute( update(Course) .where(Course.id course_id, Course.enrolled_count Course.capacity) .values(enrolled_countCourse.enrolled_count 1) ) if result.rowcount 0: raise ValueError(课程已满) enrollment Enrollment(student_idstudent_id, course_idcourse_id) db.session.add(enrollment) db.session.commit()update语句的where条件里带了enrolled_count capacity数据库在执行更新时会锁定该行保证检查和更新是原子的。如果rowcount为 0说明条件不满足课程已满抛出异常回滚。这个写法在课程设计里属于加分项答辩时能讲清楚「为什么先查后改在并发下不安全」以及「条件更新如何解决这个问题」比单纯说「我加了 try except」要有说服力得多。6.2 用 Flask 蓝图拆分路由便于维护所有路由都写在app.py里代码超过三百行之后会很难维护。用蓝图按功能拆分# auth.py from flask import Blueprint, request, jsonify, session auth_bp Blueprint(auth, __name__) auth_bp.route(/api/login, methods[POST]) def login(): # 登录逻辑 pass # course.py course_bp Blueprint(course, __name__) course_bp.route(/api/courses) def course_list(): # 课程列表逻辑 pass # app.py from auth import auth_bp from course import course_bp app.register_blueprint(auth_bp) app.register_blueprint(course_bp)蓝图的好处是每个模块的代码独立修改登录逻辑不会影响到课程模块。注册时还可以给蓝图加url_prefix比如app.register_blueprint(course_bp, url_prefix/course)这样课程相关的接口都自动带上/course前缀。6.3 用环境变量管理配置项数据库路径、密钥、调试开关这些配置不应该硬编码在代码里。用环境变量管理import os app.config[SECRET_KEY] os.environ.get(SECRET_KEY, dev-key-for-testing) app.config[SQLALCHEMY_DATABASE_URI] os.environ.get( DATABASE_URL, sqlite:///data.db) app.config[DEBUG] os.environ.get(FLASK_DEBUG, 0) 1本地开发时用默认值部署到服务器时通过环境变量覆盖。SECRET_KEY用于 session 签名生产环境必须换成随机字符串不能保留默认值。我自己的习惯是每次拿到一份课程设计源码先跑通再按上面这些点逐个检查。能跑起来只是及格线把事务、蓝图、配置管理这些细节处理好答辩时才有东西可讲。希望帮到你。本文还有配套的精品资源点击获取
返回列表