
简介基于Python开发的图书馆自动预约座位系统面向高校在校学生、毕业设计与课程设计人群解决广州大学图书馆座位预约需要频繁手动操作的痛点。项目借助GitHub Actions定时任务在每周五、周六清晨自动预约次日座位并可选用pushplus推送加微信公众号接收预约成功与否的通知整体实现简单便于二次开发与学习。压缩包共18个文件主要为Python源码、GitHub Actions工作流配置文件、项目配置文件、使用说明文档和操作截图等其中截图清晰展示配置步骤说明文档与源码配合便于快速上手调试包体总大小约1.73MB。已有628人学习下载适合计算机相关专业学生用于课设作业、期末大作业或毕设立项演示也可在此基础上扩展选座策略、多账号管理等功能提升项目完整度。1. 图书馆自动预约座位系统为什么“抢座”慢往往是业务逻辑的问题拿到一份“基于Python开发的图书馆自动预约座位系统源码”大多数人的第一反应是赶紧装依赖、跑起来、点两下页面然后得出结论“这个源码挺完整”。等真正用起来才发现“自动预约”四个字的难点根本不在界面而在座位状态怎么流转、预约请求怎么防冲突、定时任务怎么不重复执行。这类系统最常见的形态是 Django 或 Flask 做后台配 Bootstrap 做前端再用定时任务替用户定点提交预约。它解决的具体问题是把靠手速抢座变成程序自动完成同时让管理员能查得到每张座位的真实使用情况。适合的人也很明确想用一个完整 Web 项目入门 Python 的开发者、想在校园网环境里落地预约系统的学生以及需要快速评估一份源码“值不值得学”的从业者。下面按实际落地顺序把这类项目拆成可复现的设计和踩坑记录。2. 预约业务先拆成四个环节状态机、数据模型、流程与权限边界拿到源码先别急着找“自动提交座位那段代码在哪”先找模型层。预约系统的所有工作量基本都集中在“什么状态下能做什么事”这条规则链上。座位什么时候能被约、预约后多久算未签到、释放后能不能立刻被下手一个人约到这些规则翻译成代码就是一个状态机和几组数据库约束。下面按状态机、数据模型、核心流程三层拆开讲。2.1 座位状态不是“空闲/占用”两个值而是一张状态机很多初学者写的预约系统座位只有两个状态空闲、占用。这个设计在单人单座、当天预约当天用的场景下够用一旦加上“提前预约”“按时签到”“超时释放”两个状态根本表达不了业务。常见做法是用一组状态机至少包含五个节点空闲、已锁定、已预约、已签到、已释放。状态含义允许进入的下一个状态什么时候发生空闲座位可预约已锁定 / 已预约初始状态、离座释放后已锁定座位被某条预约单占用已预约、已释放用户提交预约瞬间已预约预约单已生成已签到、未签到、已释放用户到馆签到、超时未到、手动取消已签到用户已经在馆已释放用户离座或预约时段结束已释放座位回到可分配池空闲定时清理任务完成我一般会建议把“已释放”设计成独立状态而不是直接从“已签到”跳回“空闲”。原因很简单释放操作往往要附带着做黑名单判断、使用时长统计、座位清洁标记这些逻辑需要一条可追溯的记录。如果直接把状态改回空闲后续排障时只能靠日志猜“这张座位到底经历了什么”。源码里如果状态字段只有两个枚举值基本可以判断这套系统只能支撑 Demo 场景上线必然要改模型做数据迁移。2.2 三个核心模型怎么建User、Seat、Reservation 的字段取舍预约系统的主力模型就是三张表座位表、预约单表、用户表。用户表可以直接复用 Django 内置的 auth.User不建议自己再造一套用户体系既浪费时间又容易留下安全漏洞。座位表和预约单表需要重点看字段设计下面是一份常见的 Django 写法。# apps/reservation/models.py from django.db import models from django.contrib.auth.models import User class Seat(models.Model): STATUS_CHOICES ( (free, 空闲), (locked, 已锁定), (occupied, 已占用), ) name models.CharField(座位编号, max_length32, uniqueTrue) area models.CharField(区域, max_length64, blankTrue) status models.CharField(状态, max_length16, choicesSTATUS_CHOICES, defaultfree) is_active models.BooleanField(可预约, defaultTrue) class Meta: ordering [area, name] class Reservation(models.Model): STATUS_CHOICES ( (pending, 已预约), (checked_in, 已签到), (released, 已释放), (absent, 未签到), ) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name预约人) seat models.ForeignKey(Seat, on_deletemodels.CASCADE, verbose_name座位) date models.DateField(预约日期) start_time models.TimeField(开始时间) end_time models.TimeField(结束时间, blankTrue, nullTrue) status models.CharField(状态, max_length16, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(预约时间, auto_now_addTrue) class Meta: constraints [ models.UniqueConstraint( fields[seat, date, start_time], nameunique_seat_slot ) ]这段模型里最值得关注的是 Reservation 上的唯一约束。它保证了“同一张座位、同一天、同一个开始时间”只能有一条预约单这是防超卖的第一道防线后面并发章节还会再提到。Seat 表不直接把“当前使用人”做成外键也是经过考虑的一张座位一天内有多个预约时段外键只能表达“现在谁在用”表达不了“未来谁约了这个座位”所以把预约关系单独抽成 Reservation座位状态只存一个短小的枚举值。如果源码里把 user 外键直接放在 Seat 表上说明这套系统大概率不支持分时段预约。字段层面还有一个容易漏的点黑名单记录。常见做法不是在 User 表上硬加一个 is_black 布尔字段而是用计数字段加状态字段比如 late_count 记录未签到次数配合一个 last_black_date 做解禁判断。布尔字段的缺点是无法表达“进入黑名单的时间”“第几次触发”“是否已经解禁”这些过程信息业务上很难处理。2.3 预约流程的本质先锁座位再写订单顺序错了必出问题预约动作看起来只是一条 INSERT实际写起来要分三步校验座位状态、占用座位、生成预约单。这三步必须放在同一个事务里顺序也不能反。下面这段代码是预约接口的核心骨架。from django.db import transaction transaction.atomic def reserve_seat(request, seat_id): # select_for_update 会锁住这一行直到事务提交 seat Seat.objects.select_for_update().get(pkseat_id) if seat.status ! free: return error(该座位当前不可预约) Reservation.objects.create( userrequest.user, seatseat, datetoday, start_timeslot_start, statuspending ) seat.status locked seat.save() return success(预约成功)先解释顺序为什么先锁行再判断状态因为“判断状态”和“修改状态”之间有时间差两个请求可能同时读到 statusfree然后同时执行 INSERT导致超卖。select_for_update 的作用是让后到的事务等前一个事务释放锁等它拿到锁时再查 status看到的就一定是前一个事务提交后的结果。这就是“先锁后查”的意义。再解释唯一约束的兜底作用即使所有开发人员都忘了写锁数据库层面的 UniqueConstraint 也能在第二次插入时抛 IntegrityError避免脏数据落库。我在看别人写的预约源码时先看有没有这两样东西只要少了一样预约高峰期大概率会出事。这里也顺便回答一个常见疑问为什么不直接用 redis 分布式锁因为单机部署、单数据库实例的场景下select_for_update 已经够用分布式锁是为多实例扩展准备的前期引入只会增加复杂度。3. 把源码在自己电脑上跑起来环境准备、依赖安装与 Django 启动命令免费 Python 源码最常见的坑不在代码本身在环境。很多“项目使用说明”只写一句“pip install -r requirements.txt”但没告诉你 Python 版本要选哪个、虚拟环境怎么建、迁移命令跑挂了看哪里。先花三分钟把环境固定下来后面所有调试才有意义。3.1 先花三分钟读依赖与入口requirements.txt 和 manage.py解压源码后第一件事不是双击 README而是看目录结构。Django 项目通常有 manage.pyFlask 项目通常有个 app.py 或 wsgi.py。如果压缩包里配套了 requirements.txt直接打开看版本约束。一个典型的预约系统依赖长这样。Django4.2 APScheduler3.10 requests2.31 python-dotenv1.0逐个说明Django 提供 Web 框架和 Admin 后台APScheduler 负责定点触发自动预约任务requests 用于模拟登录、提交预约请求也就是“自动”那部分python-dotenv 用来读取 .env 文件里的敏感配置比如统一身份认证的账号密码。如果源码里还有爬虫相关代码requests 基本不会缺席。还有一点容易被忽略注意 requirements.txt 里如果出现 Pillow通常是 Admin 后台需要处理头像上传不影响核心业务装不上时可以先注释掉再排查。发现没有 requirements.txt 也不用慌常见做法是把源码先读一遍从 import 语句反推依赖然后手动整理一份。我一般会顺手把 Python 版本也记在 README 里Django 4.2 要求 Python 3.8 以上如果你机器上装的是 3.12大部分老源码都能跑但有些用了pytz或uwsgi的会报兼容错误。用 VSCode 调试的人记得装完依赖后把解释器切到虚拟环境路径否则表面装了包编辑器里还是报“找不到模块”。3.2 用一组命令把项目跑起来虚拟环境、迁移与第一个请求下面这组命令是 Django 项目的标准启动流程适合绝大多数源码。Windows 和 macOS 只在激活虚拟环境那一步有区别。cd 解压后的项目目录 python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate python -m pip install -r requirements.txt python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver逐条解释含义。python -m venv .venv创建隔离环境避免把包装进系统 Pythonactivate之后终端提示符会多出 (.venv)说明当前环境生效。pip install如果速度慢可以在命令里加国内镜像源参数。makemigrations根据模型生成迁移文件migrate把迁移应用到数据库这两步的区别是前者生成“改表方案”后者执行“改表动作”。createsuperuser创建管理员账号用于访问 Admin 后台。最后runserver启动开发服务器默认监听 127.0.0.1:8000。这一步最容易翻车的地方是迁移报错。常见错误有两种一是提示“表已存在”多半是源码里自带了开发数据库文件比如 db.sqlite3而模型和库里的版本不一致这时把本地开发库删掉重建即可二是提示“字段类型不兼容”比如源码用的 SQLite 而你换了 PostgreSQL通常是 CharField 长度或时间字段的差异先回退到源码默认的数据库配置跑通后再考虑迁移。记住一条原则第一次跑项目永远先用源码默认配置不要一上来就改数据库。3.3 先别写页面用 Django Admin 把预约流程调通很多源码自带完整前端页面但前端好不好看跟业务能不能跑通是两回事。我习惯先跳过页面直接用 Django Admin 做手工冒烟测试。只要在 admin.py 里注册两个模型就能在后台完成“建座位、改状态、建预约单”的完整操作。# apps/reservation/admin.py from django.contrib import admin from .models import Seat, Reservation admin.register(Seat) class SeatAdmin(admin.ModelAdmin): list_display (name, area, status, is_active) list_filter (area, status) admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): list_display (seat, user, date, start_time, status) list_filter (status, date) date_hierarchy date这段代码的作用一目了然SeatAdmin 让管理员能按区域和状态筛选座位ReservationAdmin 能按状态和日期筛选预约单date_hierarchy 自动生成日期钻取导航。手动建一条预约单时重点观察状态字段是否按预期变化新建预约单是 pending签到后改成 checked_in超时释放后变成 absent 或 released。如果发现某个流转死活改不过去说明源码里写了状态校验逻辑而这种校验恰恰是业务的核心规则。用 Admin 调通还有一个好处不需要先把前端模板的 bug 修完才能验证后端。前端页面经常因为静态文件路径、模板变量拼写问题跑不起来而 Admin 是 Django 自带的后台界面只要模型注册了就能用。先把后端业务确认无误再回头修页面排错范围能缩小一半。4. 真正实现“自动”预约登录态保持、定时调度与并发抢占策略“自动预约”这四个字完整链路是程序保持登录态 → 每天定点触发 → 在高峰并发里抢到座位 → 成功后确认。每一步都对应一类经典问题。下面的方案不针对某一个具体的图书馆系统而是按最常见的场景拆解项目本身是预约平台定时任务代用户执行预约动作如果目标是模拟登录校外预约系统核心思路也完全一样。4.1 用 requests.Session 保持登录态借爬虫思路管理 Cookie预约的前提是“已登录”。最可靠的登录方式是用 requests.Session 模拟一次表单提交让 Session 对象自动保存服务端返回的 Cookie后续所有预约请求都携带同一份身份凭证。import os import requests from dotenv import load_dotenv load_dotenv() def login_and_get_session(): session requests.Session() login_url os.getenv(LOGIN_URL) payload { username: os.getenv(STUDENT_ID), password: os.getenv(PASSWORD), } resp session.post(login_url, datapayload, timeout10) if resp.status_code ! 200: raise RuntimeError(f登录失败HTTP {resp.status_code}) return session这段代码里requests.Session()是整个方案的钥匙。Session 对象内部维护了一个 CookieJar服务端 Set-Cookie 之后下一次用同一个 Session 发请求会自动带上。timeout10是必须写的参数否则请求遇到连接卡顿时程序会无限期挂起。把账号密码放在.env而不是写死在代码里一方面是防止源码上传到公共仓库时泄露另一方面也让不同用户使用时不用改代码。如果目标系统登录时有验证码或密码加密常见做法是先抓包确认登录接口的字段名和加密方式再用 Python 模拟同样的逻辑这属于爬虫基本功。4.2 在 Django 里挂定时任务为什么用 BackgroundScheduler 而不是 BlockingScheduler定时任务是“自动预约”的触发器。APScheduler 是最常用的调度库但很多人一上来就用 BlockingScheduler结果 runserver 启动后页面卡死。原因很简单BlockingScheduler 会阻塞当前线程而 Django 开发服务器只有一个主线程被调度器占住后自然无法处理 HTTP 请求。正确做法是用 BackgroundScheduler它把调度逻辑放在后台线程。# apps/reservation/jobs.py from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger scheduler BackgroundScheduler(timezoneAsia/Shanghai) def auto_reserve_job(): session login_and_get_session() reserve(session, seat_id1024) def start_scheduler(): if not scheduler.running: scheduler.add_job( auto_reserve_job, triggerCronTrigger(hour22, minute0, second0), max_instances1, coalesceTrue, misfire_grace_time60, idauto_reserve_daily ) scheduler.start()参数说明是这套代码的关键。CronTrigger(hour22, minute0, second0)表示每天 22 点整触发和 Linux crontab 的写法思路一致max_instances1防止上一次任务还没跑完下一次又开始避免重复提交预约coalesceTrue表示如果因为系统休眠等原因积压了多次触发只执行最近一次misfire_grace_time60允许任务在规定时间后 60 秒内补执行容忍数据库慢查询造成的延迟。这里还有个坑scheduler 是模块级单例如果被多次 import可能重复注册任务所以start_scheduler里先判断scheduler.running再添加。4.3 抢座并发控制select_for_update 之外的三个补充手段第 2.3 节提过 select_for_update 锁行这里再把完整策略补齐。抢座场景的特点是短时间大量请求打同一个接口单纯靠“先查后插”必出超卖但只靠一把锁又可能拖慢吞吐。我一般会做三层补充。第一层是数据库唯一约束。Reservation 模型上的 UniqueConstraint 是最后一道闸门就算代码逻辑有漏洞数据库也不会允许插入两条相同座位、相同日期、相同时段的记录。插入时捕获 IntegrityError返回“该时段已被预约”而不是直接抛 500。from django.db import IntegrityError try: Reservation.objects.create( userrequest.user, seatseat, datetoday, start_timeslot_start, statuspending ) except IntegrityError: return error(该时段已被预约)第二层是重试策略。抢座失败不一定是座位被抢走可能是网络抖动或临时锁等待超时所以常见做法是随机退避重试每次延迟 0.5 到 2 秒最多重试三次。注意重试要带随机性如果所有用户都在固定时间重试反而会造成新的并发尖峰。第三层是锁的粒度。select_for_update 锁的应该是 Seat 表对应行而不是整张表或整个 Reservation 表。有些源码为图省事在视图一进来就锁全表预约一慢所有人都跟着慢。把锁粒度控制在单行同时保持事务尽量短锁行、判断、插入、更新状态、提交一气呵成不要在大事务里做网络请求或复杂计算。5. 图书馆预约系统常见问题与避坑五个上线前必须确认的细节这一章把开发这类系统时最容易踩的五个坑按“现象 → 原因 → 解决”写清楚。每一条都是从实际运行中积累出的血泪经验比源码本身更能决定项目能不能撑过正式使用。5.1 现象预约成功却签不了到——时区错乱导致的时间窗口位移有用户反馈明明预约时段是 14:00 到 16:0013:50 到馆却提示“未到签到时间”而管理员后台看记录又是正常的。这种现象十有八九是时区问题。Django 默认开启 USE_TZTrue数据库里存的是 UTC 时间前端展示时如果混用了本地时间就会产生 8 小时偏移。更隐蔽的是源码里可能一部分代码用datetime.now()另一部分用django.utils.timezone.now()两者相差 8 小时导致签到判断逻辑错乱。解决方法是统一时区在 settings.py 里固定TIME_ZONE Asia/Shanghai保持USE_TZ True并且业务代码里一律使用timezone.now()不混用原生 datetime。改造时有个小技巧全局搜datetime.now凡是出现在模型、视图、定时任务里的一律替换成timezone.now。这一步做完时间窗口位移的坑基本就堵住了。5.2 现象同一个座位被两个人同时约到——高并发下的超卖高峰期日志里明明看到两条预约都返回成功数据库里也出现两条相同座位相同时段的记录。原因很直接视图没有锁行也没有唯一约束两个请求同时读到 statusfree同时执行 INSERT。这个问题在测试阶段很难暴露因为手动操作不会在同毫秒内提交两次。解决方法是双保险视图层用select_for_update锁行数据库层用 UniqueConstraint 兜底。还要注意一点SQLite 在并发写场景下支持很弱超过几十个并发写请求就可能报“database is locked”如果源码默认用 SQLite测试没问题正式使用前务必备份数据并迁移到 PostgreSQL 或 MySQL。先改数据库再压测是这套系统上线前最值得投入的一步。5.3 现象定时任务不执行或重复执行——APScheduler 生命周期与多进程冲突自动预约任务有时候到点没跑有时候又跑了两遍。前者通常是把调度器写在某个模块里但 Django 的 WSGI 进程没有触发该模块的初始化后者是用了 Gunicorn 多 worker每个进程都独立启动了一个 BackgroundScheduler导致同一个任务被多个进程同时执行。解决方法是给调度器加一个独立开关只在指定的进程里启动。我一般这样处理在 app 的 ready() 里读取环境变量只有设置了RUN_SCHEDULERtrue才调用 start_scheduler。# apps/reservation/apps.py from django.apps import AppConfig class ReservationConfig(AppConfig): name apps.reservation def ready(self): import os if os.getenv(RUN_SCHEDULER) true: from .jobs import start_scheduler start_scheduler()这个开关的价值在于本地开发时不设环境变量定时任务不启动方便手动测试部署时只在其中一个 worker 上开启避免重复触发。配合前面 jobs.py 里的 max_instances1基本能压住重复执行问题。还有一个相关坑如果代码里同时存在 APScheduler 和 Django 自带的 crontab 任务可能出现双重触发检查所有入口文件确认调度器只初始化一次。5.4 现象黑名单误伤正常用户——释放逻辑把“未签到”和“迟到”混在一起用户预约了 14:00 到 16:0013:50 到馆并成功签到第二天却被拉进黑名单。查后台发现释放任务判断“当前时间大于结束时间且状态还是 pending”就自动标记 absent但它没排除已经签到过的记录。也就是释放逻辑把所有“预约单还在 pending”的用户一刀切判成缺席没考虑签到和释放之间的竞争条件。解决的关键是更新时带状态条件。执行释放时只对“状态仍为 pending 且已超时”的记录做更新更新结果返回 0 行就说明该预约单已经被签到或释放不能标记缺席。updated Reservation.objects.filter( pkreservation.pk, statuspending # 关键只处理还没签到、还没释放的单子 ).update(statusabsent) if updated 0: return # 已被签到或释放跳过黑名单判断这里也提醒一个问题状态判断要有优先级。先判断 checked_in再判断 released最后才轮到 absent。顺序写反了正常签到用户就会遭殃。释放任务应该是幂等的同一张单子无论被扫描多少次结果都一致不会因为重复执行产生误伤。5.5 现象数据库连接被占满——默认开发配置撑不住抢座高峰页面开始转圈日志里刷“database is locked”或“too many connections”。原因分两种用 SQLite 的话它对写锁是全局互斥的抢座高峰大量写事务排队直接锁库用 MySQL 的话默认连接数配置通常只有 151而开发环境常用默认连接池几十个并发请求就能打满。解决方法是分阶段调整。SQLite 阶段先确认所有写操作都在事务里且事务尽量短避免长事务持锁上线前把数据迁移到 MySQL 或 PostgreSQL并给 Django 配置连接池。MySQL 阶段把 max_connections 调高同时检查是否有慢查询常见隐患是 Booking 表缺少 date 和 status 的联合索引。这个坑最容易被忽略因为免费源码默认配置从来只为“能跑”设计不为“扛住”设计。6. 让 Demo 变成可信赖的预约系统并发验证脚本与源码检查清单前面几章把设计和坑位讲透了最后一章给两个验证手段用来判断一套源码到底是一个能跑的 Demo还是一套敢让别人用的系统。6.1 用一段脚本模拟五十个并发用户测出真实吞吐量手工测试永远测不出并发问题最直接的办法是写一段压力脚本把同一个座位 ID 同时发给预约接口。# scripts/concurrent_reserve_test.py from concurrent.futures import ThreadPoolExecutor import requests BASE_URL http://127.0.0.1:8000 TOKEN 替换为实际登录后的凭证 def try_reserve(seat_id): session requests.Session() session.headers.update({Authorization: TOKEN}) resp session.post(f{BASE_URL}/api/reserve/{seat_id}/, timeout5) return resp.status_code with ThreadPoolExecutor(max_workers50) as pool: results list(pool.map(try_reserve, [1024] * 50)) print(200 数量:, results.count(200)) print(非 200 数量:, sum(1 for r in results if r ! 200))这段脚本用 ThreadPoolExecutor 并发发出 50 个预约请求全部打到同一个座位上。观察点有两个200 的数量必须是 1说明超卖被拦住其余 49 个请求应该返回 4xx 或业务错误码而不是 500。如果 200 数量大于 1说明锁或唯一约束没生效回到 4.3 节的方案去补。max_workers50 和 timeout5 是常用初始值实测时可以根据服务器配置调整。6.2 再读源码时的检查清单安全、边界、可维护三个角度检查角度具体看什么合格标准安全登录密码是否明文写死、接口是否校验登录态、.env 是否被提交敏感配置不外露非管理员不能操作预约记录边界状态机是否覆盖未签到、迟到、提前离座每个非法状态转换都有明确错误提示不产生脏数据可维护定时任务是否集中管理、时间比较是否统一调度配置集中在 jobs 模块业务代码只用 timezone.now()我拿到任意一份预约系统源码都会按这个顺序过三关第一关全新虚拟环境里依赖能不能装干净第二关Admin 后台手工跑一遍状态流转第三关并发脚本打一轮看是否超卖。三层都过才敢说这套源码“值得学习”。线上遇到过太多先把页面改得花团锦簇、最后却倒在并发入库的项目与其急着重构不如先把最底层的状态机和数据约束确认清楚。希望帮到你。本文还有配套的精品资源点击获取