☰
Django兴趣班预约系统源码解析:从数据模型到并发事务的毕设实战
2026/10/4 4:29:21 网站建设 项目流程

简介:这份资源是面向高校学生与编程学习者的Django兴趣班预约管理系统完整项目包,可作为毕业设计、课程设计、大作业或工程实训的参考方案,帮助解决选题难、功能模块不完整、缺少可运行代码等问题。压缩包共735个文件,约19.91MB,涵盖39个py后端源码、41个vue前端组件、53个css样式、164个js脚本以及sql数据库文件、docx与pptx文档等,前后端分离结构清晰,便于按模块阅读与二次开发。系统基于Python3.7、Django、MySQL5.7与Vue实现,包含管理员、教师、学生三种角色:管理员负责教师、学生、课程、公告等信息的增删改查与批量操作;教师可查看课程并审核学生的预约与取消申请;学生可搜索课程、提交预约时长与原因、查看审核状态并取消预约。已有1891人学习下载,适合希望掌握Django全栈开发、理解预约审核业务流程的进阶学习者参考借鉴。

1. 从一份 Django 兴趣班预约系统源码说起:它到底能解决什么问题

每到学期初,培训机构的教务老师最头疼的不是排课,而是家长在微信群里刷屏报名——"周三下午四点还有名额吗""我家孩子换到周六上午行不行"。手工登记在 Excel 里,改一次要翻三张表,退课还得挨个核对。基于 Django 的兴趣班预约管理系统,本质就是把"课程—时段—名额—学员"这四件事用数据库约束住,让报名、退课、名额扣减在一个事务里完成,而不是靠人脑记。这套毕设源码通常包含 Django 工程、SQL 建表脚本和一份设计文档,适合两类人:一是正在做 Python 毕设、需要一套能跑通、能答辩的完整项目;二是想借一个小型业务系统把 Django 的 ORM、模板、表单校验串起来的新手。它不复杂,但麻雀虽小,预约冲突、并发扣名额、状态流转这些真实问题一个都不少,正好拿来练手。

2. 拆开这套 Django 预约系统:数据模型与核心表怎么设计

2.1 四张核心表撑起整个预约逻辑

不管源码里表名怎么起,兴趣班预约的骨架跑不出这四张表:课程表(Course)、班次/时段表(Schedule 或 ClassSlot)、学员表(Student)、预约记录表(Booking)。课程表存课程名、老师、总课时;班次表存具体上课时间、容量上限、已报名人数;学员表存姓名、联系方式;预约记录表是核心,它把学员和班次关联起来,并带一个状态字段(待确认/已确认/已取消)。

这里有个设计取舍:已报名人数到底存不存。存的话查询快,但每次报名都要同步更新,容易和预约记录对不上;不存的话每次实时 count,数据永远准,但班次多了会慢。我一般建议在班次表留一个enrolled字段做冗余,同时用数据库事务保证它和预约记录一致,这也是大多数毕设源码的做法。

# models.py 核心模型示例 from django.db import models class Course(models.Model): name = models.CharField(max_length=100, verbose_name="课程名") teacher = models.CharField(max_length=50, verbose_name="授课老师") total_lessons = models.IntegerField(default=0, verbose_name="总课时") class Schedule(models.Model): course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name="所属课程") start_time = models.DateTimeField(verbose_name="上课时间") capacity = models.IntegerField(default=20, verbose_name="容量上限") enrolled = models.IntegerField(default=0, verbose_name="已报名人数") # 冗余字段 class Student(models.Model): name = models.CharField(max_length=50) phone = models.CharField(max_length=20, unique=True) class Booking(models.Model): STATUS = (('pending', '待确认'), ('confirmed', '已确认'), ('canceled', '已取消')) student = models.ForeignKey(Student, on_delete=models.CASCADE) schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE) status = models.CharField(max_length=10, choices=STATUS, default='pending') created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('student', 'schedule') # 防止同一人重复预约同一班次

逻辑说明:unique_together是防重复预约的第一道闸,数据库层面直接拦住同一学员对同一班次的二次提交。enrolled冗余字段配合事务使用,报名时先select_for_update锁行再自增,避免并发下超卖。参数上,capacity默认给 20 是常见小班规模,实际按机构情况改;on_delete=models.CASCADE表示课程删了班次跟着删,如果业务上不允许删课程,改成PROTECT更稳妥。

2.2 用 SQL 建表脚本对照理解 ORM 到底生成了什么

毕设包里一般会附一份.sql文件,很多人直接导入就不管了。其实拿它和 Django 的makemigrations生成结果对照,是理解 ORM 最快的方式。下面这段是预约记录表在 MySQL 里的典型建表语句:

CREATE TABLE `booking` ( `id` bigint NOT NULL AUTO_INCREMENT, `status` varchar(10) NOT NULL DEFAULT 'pending', `created_at` datetime(6) NOT NULL, `schedule_id` bigint NOT NULL, `student_id` bigint NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uniq_student_schedule` (`student_id`, `schedule_id`), KEY `booking_schedule_id_fk` (`schedule_id`), CONSTRAINT `booking_schedule_id_fk` FOREIGN KEY (`schedule_id`) REFERENCES `schedule` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:UNIQUE KEY对应模型里的unique_together,FOREIGN KEY对应ForeignKey。注意student_id和schedule_id上单独建了索引,因为按学员查"我报了哪些课"、按班次查"这个班有谁"都是高频查询。字符集用utf8mb4而不是utf8,否则学员姓名里的生僻字或 emoji 会报错,这是血泪经验。参数上bigint是 Django 3.2 之后 AutoField 的默认类型,老版本可能是int,导入脚本前先确认版本对得上。

2.3 报名扣名额:一个必须放进事务的操作

报名这个动作看着简单,实际包含三步:查班次是否还有名额、写预约记录、班次已报名数加一。三步之间只要有一丝并发,就会出现"名额显示还剩 1 个,两个人同时点都成功了"的超卖。正确做法是放进transaction.atomic()并对班次行加锁:

from django.db import transaction from django.core.exceptions import ValidationError def book_schedule(student_id, schedule_id): with transaction.atomic(): # select_for_update 锁住这一行,其他事务排队等待 schedule = Schedule.objects.select_for_update().get(id=schedule_id) if schedule.enrolled >= schedule.capacity: raise ValidationError("该班次名额已满") if Booking.objects.filter(student_id=student_id, schedule_id=schedule_id).exists(): raise ValidationError("你已预约过该班次") Booking.objects.create(student_id=student_id, schedule_id=schedule_id, status='confirmed') schedule.enrolled += 1 schedule.save(update_fields=['enrolled'])

逻辑说明:select_for_update()是行级锁,必须放在事务里才生效,它保证同一时刻只有一个请求能读到并修改这个班次。update_fields=['enrolled']只更新这一个字段,避免把整个对象写回去覆盖别的并发修改。参数上,如果用的是 SQLite,select_for_update支持有限,毕设演示够用,但真上生产建议换 MySQL 或 PostgreSQL。退课时反向操作,把enrolled减一,同时把预约记录状态改成canceled,而不是直接删记录——保留历史才能查"谁什么时候退的"。

3. 把项目在本地跑起来:环境、依赖与启动步骤

3.1 Python 与 Django 版本怎么选才不翻车

拿到源码第一步不是急着runserver,而是先看requirements.txt或文档里写的 Django 版本。Django 2.x、3.x、4.x 之间差异不小,比如url()在 4.0 之后基本被path()取代,ugettext_lazy也改了名。新手最容易踩的坑就是 Python 3.10 配 Django 2.2,直接报兼容错误。稳妥组合是 Python 3.8~3.10 配 Django 3.2(LTS 长期支持版),或者 Python 3.10+ 配 Django 4.2。安装命令:

# 建议先建虚拟环境,别污染全局 python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 安装依赖,版本以 requirements.txt 为准 pip install django==3.2.18 pip install mysqlclient # 用 MySQL 时 # 或者用轻量的 sqlite,毕设演示足够

逻辑说明:虚拟环境是后悔药,装崩了直接删掉重建,不影响系统 Python。mysqlclient在 Windows 上编译经常失败,替代方案是pymysql,在__init__.py里加pymysql.install_as_MySQLdb()即可。参数上,如果只是本地跑通看效果,直接用 SQLite 最省事,把settings.py里DATABASES的 ENGINE 改成django.db.backends.sqlite3,连数据库都不用装。

3.2 数据库配置与 SQL 文件导入

如果坚持用 MySQL,先在 Navicat 或命令行里建库,再导入附带的.sql文件。注意导入顺序:先建库、再导表结构、最后导数据(如果有)。命令行方式:

# 建库,字符集必须是 utf8mb4 mysql -u root -p -e "CREATE DATABASE interest_class DEFAULT CHARSET=utf8mb4;" # 导入 sql 文件 mysql -u root -p interest_class < interest_class.sql

逻辑说明:DEFAULT CHARSET=utf8mb4一定要写,否则中文和特殊字符会乱码。导入后进 Django 的settings.py改数据库连接:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'interest_class', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }

参数说明:OPTIONS里的charset和建库时保持一致,两边不一致照样乱码。如果导入 SQL 后运行migrate报"表已存在",说明 SQL 文件里已经建好了表,这时要么跳过 migrate,要么先删表让 Django 自己建。常见做法是:SQL 文件只用来参考表结构,实际让 Django 通过migrate生成,避免两边字段对不上。

3.3 迁移、建管理员、启动服务

数据库通了之后,标准三步走:

python manage.py makemigrations # 生成迁移文件 python manage.py migrate # 应用到数据库 python manage.py createsuperuser # 建后台管理员,按提示输账号密码 python manage.py runserver # 启动,默认 127.0.0.1:8000

逻辑说明:makemigrations把模型变化翻译成迁移脚本,migrate才真正动数据库,两步分开是为了让你有机会检查生成的 SQL 对不对。createsuperuser建的是 Django Admin 后台账号,登录/admin就能直接管理课程、班次、预约记录,毕设答辩时演示这个后台很加分。启动后如果页面样式全丢,多半是静态文件没配好,检查settings.py里STATIC_URL和模板里的引用路径。

4. 预约系统避坑与排查:五个真实翻车现场

4.1 现象:两个人同时报名,名额超了

原因:报名逻辑没加事务和行锁,两个请求都读到enrolled=19,都判断"没满",都加一,结果变成 21。解决:按 2.3 的方式用transaction.atomic()包住,并对班次行select_for_update()。验证方法是开两个浏览器同时点,或者写个简单脚本并发请求,看最终enrolled有没有超过capacity。

4.2 现象:中文姓名存进去变成问号

原因:数据库、表、连接三处字符集不统一,常见是建库用了utf8而连接没指定。解决:建库用utf8mb4,settings.py的OPTIONS里加charset: utf8mb4,已有表用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4转换。这个坑在导入别人 SQL 文件时尤其常见,因为文件本身的编码你控制不了。

4.3 现象:退课后名额没释放,别人报不进来

原因:退课只改了预约记录状态,忘了把班次的enrolled减一;或者减了但没放进同一个事务,中途报错导致数据不一致。解决:退课和报名对称处理,同一个事务里改状态、减计数。另外注意别用delete()删预约记录,删了就没法追溯,用状态字段标记取消才是正路。

4.4 现象:migrate报 no such column 或表不存在

原因:模型改了但没生成迁移,或者 SQL 文件建的表和模型定义对不上。解决:先makemigrations看有没有新迁移,再migrate。如果 SQL 文件和模型冲突,以模型为准,删掉冲突的表让 Django 重建。排查时用python manage.py showmigrations看哪些迁移没应用,用python manage.py sqlmigrate app 0001看具体会执行什么 SQL。

4.5 现象:后台能登录但看不到预约数据

原因:模型没注册到 admin,或者注册了但list_display里写了不存在的字段。解决:在对应 app 的admin.py里用@admin.register(Booking)注册,list_display只写模型里真实存在的字段名。如果报字段错误,Django 会明确告诉你哪个字段找不到,照着改就行。

5. 让这套系统更像"能交付"的作品:三个进阶技巧

5.1 用状态机管住预约流转,别让状态乱跳

毕设里最常见的偷懒是把status当普通字符串随便改,结果出现"已取消的预约又被确认"这种脏数据。进阶做法是给状态流转加约束:只允许pending → confirmed、pending → canceled、confirmed → canceled,其他一律拒绝。可以在模型save()里判断,也可以单独写个transition方法:

ALLOWED = { 'pending': ['confirmed', 'canceled'], 'confirmed': ['canceled'], 'canceled': [], } def change_status(booking, new_status): if new_status not in ALLOWED.get(booking.status, []): raise ValidationError(f"不允许从 {booking.status} 变到 {new_status}") booking.status = new_status booking.save(update_fields=['status'])

逻辑说明:ALLOWED字典把合法流转写死,任何越界操作直接抛异常。这样即使前端传了乱七八糟的状态值,后端也拦得住。参数上,如果业务需要"取消后重新预约",那就不是改状态,而是新建一条记录,历史记录保持只读。

5.2 名额查询用 annotate 一次算清,别在模板里循环查

新手常犯的错是在模板里对每个班次schedule.booking_set.count(),班次一多就是 N+1 查询,页面卡到怀疑人生。正确做法是用annotate在数据库层一次算完:

from django.db.models import Count, F schedules = Schedule.objects.annotate( booked=Count('booking', filter=Q(booking__status='confirmed')), remaining=F('capacity') - F('enrolled') ).filter(remaining__gt=0)

逻辑说明:annotate把统计下推到 SQL,一次查询拿到所有班次的已报名数和剩余名额,filter(remaining__gt=0)直接筛出还有位置的班次。参数上,filter=Q(...)只统计已确认的预约,待确认和已取消的不算数,这个细节决定了名额显示准不准。

5.3 答辩前必做的一次完整回归

我自己的习惯是,交付前一定手动走一遍全流程:建课程 → 建班次 → 学员报名 → 名额减一 → 退课 → 名额加一 → 后台看记录状态。每一步都截图存好,答辩时按这个顺序演示,比临场乱点稳得多。另外把DEBUG在演示环境保持True方便看报错,但文档里要写清楚生产环境必须改False并配ALLOWED_HOSTS,这是基本素养。数据库记得提前备份一份,演示前如果数据被点乱了,直接还原,别在现场手忙脚乱地改。这套系统不难,难的是把边界情况都想到,把状态和数据一致性守住——这也是它能写进简历、经得起追问的原因。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询