
简介PythonDjango自习室预约系统项目源码包专为高校计算机相关专业师生及开发者设计可直接用于课程设计、毕业设计或项目实战演练。系统基于Django框架构建涵盖自习室信息管理、预约流程处理、用户权限控制等核心模块整体功能完整、运行稳定。资源包共55个文件主要由25个Python源码、9个HTML模板、6个XML配置、3个CSS样式及SQL数据库脚本、Dockerfile等组成压缩包仅482KB结构清晰、轻量便携。包内同步提供设计报告文档、使用说明、依赖清单和数据库初始化脚本便于快速搭建环境、理解设计思路并在此基础上进行二次开发或功能扩展。目前已有55人学习下载适合新手入门参考也适合需要高质量课设/毕设成果的开发者借鉴。1. 从抢座位到可复现的 Django 预约链学期末的自习室管理本质是一场规则清晰的资源争夺战座位固定、时段固定核心问题永远是“谁占了这个座位、占多久、没来怎么办”。把它做成一个 Django 项目最关键的并不是页面做得有多花哨而是把“座位在某时段是否被占用”变成可以查询的数据再把预约、取消、签到、过期释放变成一系列不会互相冲突的状态迁移。这套基于 PythonDjango 的自习室预约系统核心业务放在名为library的 Django 应用中配套 MySQL 数据库脚本library.sql、docker-compose.yml部署文件以及UITest/selenium1.py自动化冒烟测试。对课程设计和毕业设计而言它最值得参考的地方在于链路完整从数据模型、视图代码、管理后台到容器化部署完全可以在本地按步骤复现。下面的内容按数据模型、预约动作、部署配置和排错优化四层拆开Django 有一定基础的人可以直接借鉴代码结构刚入门的人跟着命令也能把环境跑起来。2. 核心数据模型自习室、座位与预约时段的叠加关系2.1 三层表结构解决“座位到底在哪”的查询难题如果把预约信息直接塞进一张“学生-座位-时间”的宽表后续要想查“A101 还剩多少个座位”就得反复在字符串里拆房间名非常别扭。这个项目拆成了三张表StudyRoom存自习室Seat存物理座位Reservation存预约时段。源码里library应用对应library.sql中的表结构核心代码大致如下from django.db import models class StudyRoom(models.Model): room_no models.CharField(max_length20, uniqueTrue) # 教室编号如 A101 capacity models.PositiveIntegerField(default0) # 座位总数 class Seat(models.Model): room models.ForeignKey(StudyRoom, on_deletemodels.CASCADE, related_nameseats) seat_no models.CharField(max_length20) # 座位号如 12 号桌 enabled models.BooleanField(defaultTrue) # 是否开放预约 class Reservation(models.Model): seat models.ForeignKey(Seat, on_deletemodels.CASCADE, related_namereservations) student_no models.CharField(max_length30, db_indexTrue) # 学号 start_time models.DateTimeField(db_indexTrue) # 预约开始时间 end_time models.DateTimeField(db_indexTrue) # 预约结束时间 STATUS_CHOICES [ (pending, 待签到), (occupied, 使用中), (finished, 已完成), (canceled, 已取消), ] status models.CharField(max_length10, choicesSTATUS_CHOICES, defaultpending)这套表结构值得注意的有两点。第一Seat通过外键挂在StudyRoom下查“A101 哪些座位可预约”时就只需要一次反向过滤room.seats.filter(enabledTrue)不需要在每张座位表里冗余房间名改动房间编号也只动一处。第二Reservation的start_time和end_time都显式加了db_indexTrue因为预约冲突判断、超时释放、时段统计全部落在时间范围过滤上没有索引时数据量稍微涨上去接口响应会明显变慢。另外要注意student_no在这里设计成普通字符串而不是外键用户表是一个刻意的取舍。课设项目中用户表的字段经常变动比如有些场景要求支持临时访客预约直接把学号做成字符串字段后续扩展会省去大量迁移和外键级联的麻烦缺点是没法用 Django ORM 直接做用户维度的关联查询需要靠查询集过滤解决。2.2 状态机比布尔字段合理在哪座位预约最常见的设计错误是只用一个is_booked布尔值表示“有没有被预约”。这种做法一旦出现“预约后没来、提前离场、管理员手动释放座位”等情况就完全说不清楚记录处于哪个阶段。本项目用status字段把预约流转拆成四个状态状态变更和触发动作如下事件状态变化关键动作提交预约(无) → pending校验时间段与已有预约不冲突到馆签到pending → occupied记录实际签到时间主动取消pending/occupied → canceled释放该时段其他人可预约使用结束occupied → finished写入结束时间超时未签到pending → canceled定时任务批量释放在这个设计下“空闲”不是一个存储字段而是对Reservation表做非重叠查询得到的结果。某个座位在特定时段是否空闲完全由该时段内是否存在pending或occupied状态的预约记录决定。这个思路贯穿整个系统判断冲突时查重叠区间展示空闲座位时排除掉重叠记录释放座位时修改状态而不是删除记录因此保留了完整的预约轨迹答辩时拿状态流转图出来讲比单纯贴一张表结构有力得多。初始化阶段library.sql里通常会预置两个自习室和十几条座位记录这样首次启动后打开管理后台就有数据可看不需要手工逐条录入。如果希望重置演示数据可以直接重新导入 SQL 脚本也可以执行python manage.py migrate后通过 Admin 后台添加。3. 预约接口与 ORM 查询一个时间窗口如何判断冲突3.1 用 Q 对象做时间区间重叠判断判断一个座位在[start_time, end_time)是否可预约核心是一个 SQL 逻辑待预约时段与已存在预约时段存在交集。两个区间重叠的条件是新开始时间 已存在结束时间 AND 新结束时间 已存在开始时间。Django 的 ORM 里要写这种组合条件Q对象是最清晰的表达方式from django.db.models import Q from .models import Reservation def is_conflict(seat, start_time, end_time): return Reservation.objects.filter( seatseat, status__in[pending, occupied], # 只统计有效占用 ).filter( Q(start_time__ltend_time) Q(end_time__gtstart_time) ).exists()这段代码里第一个filter先把finished和canceled的记录剔除因为它们对应的时段已经释放第二个filter的Q(start_time__ltend_time)判断已有预约的开始时间是否早于新预约的结束时间Q(end_time__gtstart_time)判断已有预约的结束时间是否晚于新预约的开始时间两个条件同时成立就说明区间重叠。用exists()而不是count()的原因是只要存在一条冲突记录就足够数据库可以提前终止扫描比查出完整记录列表再判断节省开销。3.2 表单层、视图层、数据库层的三层校验实际预约功能不能只靠冲突函数真正的课设项目通常会在三层各设一道校验拦截维度不同各有分工校验层级校验内容实现方式失败返回表单层时间格式、end_time晚于start_time表单字段校验 /clean()400提示结束时间不合法视图层时间段冲突、同人重复占座filterQ条件组合409提示该时段已被预约数据库层并发下的重复写入select_for_update()行锁事务回滚接口返回 409视图层是主战场一个带事务包裹的预约创建逻辑如下from django.db import transaction from django.http import JsonResponse from django.utils import timezone from .models import Seat, Reservation from .utils import is_conflict def create_reservation(request): seat Seat.objects.get(pkrequest.POST.get(seat_id)) start_time timezone.datetime.fromisoformat(request.POST.get(start)) end_time timezone.datetime.fromisoformat(request.POST.get(end)) if end_time start_time: return JsonResponse({code: 400, msg: 结束时间必须晚于开始时间}) if is_conflict(seat, start_time, end_time): return JsonResponse({code: 409, msg: 该时段已被预约}) with transaction.atomic(): Reservation.objects.create( seatseat, student_norequest.POST.get(student_no), start_timestart_time, end_timeend_time, statuspending, ) return JsonResponse({code: 200, msg: 预约成功})这里的fromisoformat在 Python 3.7 之后可以直接解析2025-06-01T08:00:00这种格式前端提交表单时用input typedatetime-local生成对应文本即可。transaction.atomic()把冲突判断和创建操作放进同一事务但这并不是并发场景的完整解决方案两个请求同时通过is_conflict判断后仍然可能在同一瞬间各自插入一条记录真正要兜底得靠第 5 章讲的行锁方案。3.3 取消预约与超时释放的边界处理取消预约要注意状态限制只有pending和occupied可以取消已经finished的记录需要保留作为历史数据def cancel_reservation(request, pk): with transaction.atomic(): res Reservation.objects.select_for_update().get(pkpk) if res.status not in (pending, occupied): return JsonResponse({code: 400, msg: 当前状态不可取消}) res.status canceled res.save() return JsonResponse({code: 200, msg: 取消成功})超时释放的常规做法是定时任务扫描把“开始时间早于当前时间 30 分钟且仍为pending”的记录批量置为canceled。这里不建议用 Python 循环逐条save()因为记录多了会产生大量单条 UPDATE 语句。Django ORM 的update()可以直接生成一条批量 UPDATEfrom datetime import timedelta from django.utils import timezone from .models import Reservation expired Reservation.objects.filter( statuspending, start_time__lttimezone.now() - timedelta(minutes30), ) expired.update(statuscanceled)update()不触发save()方法也不会调用模型的信号性能比循环快一个数量级缺点是无法保留每条记录的修改时间如需审计日志要另外设计now字段。4. 跑通部署MySQL、Django Admin 与容器化配置4.1 Docker Compose 同时拉起 MySQL 和 Web 服务项目附带的docker-compose.yml和Dockerfile解决的是“本机没装 MySQL课设演示环境不好迁移”的痛点。一个典型的 Compose 编排如下version: 3 services: db: image: mysql:8.0 environment: MYSQL_DATABASE: library MYSQL_ROOT_PASSWORD: root ports: - 3306:3306 volumes: - ./mysql:/docker-entrypoint-initdb.d web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - 8000:8000 depends_on: - dbMYSQL_DATABASE指定容器首次启动时自动创建的数据库名MYSQL_ROOT_PASSWORD是本地联调密码正式环境一定要换成环境变量引用。./mysql目录挂载到/docker-entrypoint-initdb.d后MySQL 容器首次初始化会按文件名字典序自动执行其中的.sql脚本library.sql的表结构、初始座位数据就是这样一次性导入的。部署环境注意depends_on只保证 db 容器先启动不保证 MySQL 已经完全就绪所以 web 容器启动时报2003 Cant connect to MySQL server是常见现象。处理方式是在 Django 入口命令前加一段等待脚本轮询3306端口确认 MySQL 接受连接后再执行migrate和runserver。4.2 settings.py 里的 MySQL 连接、字符集与时区真正把 Django 切换到 MySQLsettings.py的数据库配置要这样改DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: library, USER: root, PASSWORD: root, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } } USE_TZ True TIME_ZONE Asia/Shanghai关键点有三个。一是ENGINE改成django.db.backends.mysql后需要安装驱动最常见的做法是pip install mysqlclientLinux 环境先安装libmysqlclient-dev系统依赖再装否则会报编译错误这也是“django install mysqlclient”相关检索里出现频率最高的问题。二是charset显式设为utf8mb4否则中文座位名称、学号等字段可能出现乱码。三是HOST要和docker-compose.yml里 db 服务映射的端口一致容器内互相访问用服务名db宿主机调试用127.0.0.1。一个经常被忽略的点是USE_TZ True时数据库中实际存储的是 UTC 时间展示时按TIME_ZONE转换成本地时间。TIME_ZONE设为Asia/Shanghai并不代表存进去就是东八区时间只是告诉 Django “以这个时区做展示转换”这直接影响查询逻辑第 5 章会专门展开。部署流程总结下来是这样几张配置之间的关系配置项示例值说明ENGINEdjango.db.backends.mysql依赖mysqlclient驱动NAMElibrary与 Compose 中MYSQL_DATABASE保持一致TIME_ZONEAsia/Shanghai配合USE_TZ True使用初始化 SQL./mysql/library.sql容器首次启动时自动导入4.3 Admin 注册与 Selenium 冒烟测试的落地写法后台管理是这类系统演示时最好用的部分用 Django Admin 自带的增删改查就可以管理房间、座位、预约记录不需要再写一套管理页面。把admin.py配置好后台界面就能直接用来做数据维护这也是管理页面里最省成本的方案from django.contrib import admin from .models import StudyRoom, Seat, Reservation admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): list_display (seat, student_no, start_time, end_time, status) list_filter (status, start_time) search_fields (student_no,) date_hierarchy start_time admin.register(Seat) class SeatAdmin(admin.ModelAdmin): list_display (room, seat_no, enabled) list_filter (room, enabled)list_display控制列表页展示哪些字段list_filter把状态变成左侧筛选项演示时按“待签到”“使用中”等状态过滤预约记录非常直观date_hierarchy会在列表顶部生成按月钻取的时间导航条查看某一天的所有预约记录很方便。配套的UITest/selenium1.py是 Selenium 冒烟测试脚本核心思路是让浏览器自动走一遍“登录 → 提交预约 → 检查结果页”的完整流程。运行前要确认 ChromeDriver 版本与浏览器主版本一致否则会直接抛出WebDriverException。测试数据推荐在setUp中清理或使用独立测试库否则预约脚本每跑一次就多一条记录第二次执行冲突判断就会失败这也是 Selenium 用例常见的稳定性问题。5. 并发、时区与慢查询预约系统的三个进阶排错点5.1 用 select_for_update 解决同时抢同一个座位上一章提到transaction.atomic()无法解决两个请求同时通过冲突判断的问题。真正兜底的做法是先把目标座位所在行锁住再执行判断和插入from django.db import transaction def create_reservation_locked(seat_id, start_time, end_time): with transaction.atomic(): seat Seat.objects.select_for_update().get(pkseat_id) if is_conflict(seat, start_time, end_time): return False, 该时段已被占用 Reservation.objects.create( seatseat, student_no20240001, start_timestart_time, end_timeend_time, statuspending, ) return True, 预约成功select_for_update()会对Seat表中对应行加排他锁第二个请求必须等待第一个事务结束才能读到该行此时再查is_conflict刚插入的预约记录就已经可见了。需要知道这个操作在 SQLite 中不可用因为 SQLite 不提供行级锁语义这也是项目必须依赖 MySQL 的原因之一。自习室预约场景的并发量通常远低于在线秒杀行锁方案完全够用。5.2 时区错位为什么查询结果比桌面时间少 8 小时USE_TZ True时Django 存入 MySQL 的是 UTC 时间而自习室业务约定的是东八区。如果查询“今天 8:00 到 22:00 有哪些时段被占用”直接把datetime(2025, 6, 1, 8, 0)丢进 ORM 过滤Django 会把这个时间当作 UTC结果自然偏移 8 小时。正确做法是让localtime()参与构造from django.utils import timezone start timezone.localtime().replace(hour8, minute0) end timezone.localtime().replace(hour22, minute0) upcoming Reservation.objects.filter( status__in[pending, occupied], start_time__ltend, end_time__gtstart, )timezone.localtime()返回的是TIME_ZONE配置的本地时间ORM 构造 SQL 时会把本地时间转换为 UTC 再比较整个过程不需要手动加减时差。调试时可以在代码里打印str(start)输出会带上08:00时区偏移据此可以确认输入输出是否在同一个时区体系内。5.3 慢查询定位与一键释放过期座位如果系统运行一段时间后发现预约列表页变慢先不要猜用 QuerySet 自带的explain()看执行计划python manage.py shell -c from library.models import Reservation; print(Reservation.objects.filter(seat_id1).explain())输出里如果type显示ALL说明走了全表扫描回模型层确认start_time、end_time是否加了db_indexTrue。另一个常见问题是列表页展示关联字段时产生 N1 查询也就是每条预约记录都额外执行一次查询拿座位和教室信息通过select_related(seat__room)可以把关联表一次性 JOIN 出来。把超时释放逻辑封装成一个 Django 管理命令部署在 Linux 服务器上交给 crontab 每小时执行就完成了自动化的最后一环# library/management/commands/release_expired.py from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone from library.models import Reservation class Command(BaseCommand): def handle(self, *args, **options): n Reservation.objects.filter( statuspending, start_time__lttimezone.now() - timedelta(minutes30), ).update(statuscanceled) self.stdout.write(freleased {n} expired reservations)命令行执行python manage.py release_expired即可看到释放数量。若在 Linux 环境部署crontab 注册为0 * * * * cd /path/to/mydjangoDemo01 python manage.py release_expired。这套组合把并发行锁、时区转换和定时清理三个问题都落到了可执行的代码层面运行后通过Reservation表中status的分布就能直观验证整个预约闭环是否正常。本文还有配套的精品资源点击获取