news 2026/9/30 16:04:02

实验室预约排课系统设计:Python+小程序从冲突检测到并发控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
实验室预约排课系统设计:Python+小程序从冲突检测到并发控制

去年学院实验室管理员找到我,说排课还是靠一张Excel表来回传,学生想预约实验时段只能到现场签字,老师调课经常撞车。我随手写了个小程序版的实验室预约排课系统,后端用Python,前端挂在小程序上,从需求梳理到部署上线跑了不到三周。这篇文章就把这个系统的完整设计思路、核心代码、排课算法和踩过的坑都梳理出来,给准备做类似预约类系统的同学一个可复用的参考。无论你是要排实验室、排机房、还是排会议室,这套设计都能直接改改就用。

1. 实验室预约排课,难点到底在哪

1.1 表面是排课,深层是资源调度问题

实验室和普通教室最大的区别在于:它既有时间维度,又有资源维度。你说的预约排课系统,本质上要做两件事——按时段分配可用实验室,按实验室校验时间段是否被占用。看起来不复杂,真做起来麻烦得多。

实验室通常有多个,每间能容纳的人数不同,设备条件也不同。同一个时间段,不同老师可能都想用同一间设备最全的实验室;同一门课,又可能一周内需要在不同实验室完成不同实验。而且实验室并非只用于课表内课程,课后还有自主预约、课外实践、竞赛集训、设备维护保养等场景。这就意味着“排课”不能像普通教室那样只做课程安排,还得兼顾开放性预约。

我的做法是先按角色捋清需求:管理员要能维护实验室信息,批量导入学期课程,审核学生的预约申请;老师要能发起课程排课,临时调停课,查看教室占用情况;学生则是查看空闲时段、提交预约、收到审批结果。整个系统角色清晰之后,代码结构自然就不会乱。

1.2 排课系统的核心边界:候补、驳回、占座、锁定

确定功能边界时,我和实验室管理员聊了很久,最终确认了几个关键机制。第一个是预约锁定期:课程排课优先于自由预约,管理员导入的课程一旦排进某时段,该时段立即从学生的可约列表中消失。第二个是预约审核机制:学生提交预约后并不会即刻生效,而是先置为待审核状态,管理员确认后变成已确认。第三个是候补队列:当某个时段已经排满,后续学生仍可提交候补申请,一旦已确认名额释放,按候补顺序自动递补。

这三个机制对应了实际实验室管理的三个痛点:上课优先、安全把关、提高利用率。代码实现上,候补队列我用了最小堆来维护,谁先预约谁优先递补,避免人为干预纠缠。审核环节则做了一个简单的状态机:待审核、已确认、已拒绝、已取消、已完成、已爽约。每个状态都是从业务场景倒推出来的,代码里对应的是状态流转表,而不是一坨if else。

1.3 技术选型:为什么是Python和小程序

后端选了Python,说实话纯粹是因为开发效率高,Flask就够用了。这个系统的并发量不大,全校同时在线也就几十到几百人,用不上FastAPI的异步优势,Flask加SQLAlchemy的组合最稳。数据库我选了SQLite起步,后续真要上并发再迁MySQL,因为SQLAlchemy的ORM层已经把大部分迁移成本吃掉了。

小程序端没什么悬念,微信小程序天然适合校园场景,学生和老师都在微信里,不用额外装App。前端用原生小程序写的,没有上uni-app,因为页面不多,原生足够,反而少了一层编译带来的排错成本。登录直接用微信的code换openid,后端签发自己的token,不依赖微信的session_key,后续扩展多端登录也方便。

2. 数据库建模:一张宽表解决80%的冲突问题

2.1 基础表结构:实验室、用户、学期课程

数据库表设计是我在这类系统里最看重的一环。排课系统的核心不是“课表”那一张表,而是把时间、地点、人物、状态拆成粒度合适的组合。我建了这几张基础表:

  • lab:存储实验室基本信息,包括编号、名称、容量、设备标签、是否可预约、维修状态。
  • user:用户表,角色字段区分admin、teacher、student。
  • semester:学期表,记录学期名称、起始日期、结束日期,排课和预约都限定在学期范围内。
  • course:课程表,维护课程名称、授课老师、关联的lab_id、上课周次、起始节次、结束节次。
  • appointment:预约记录表,这是整个系统的核心,后面单独说。

课程表和预约表是分开的,课程排课算作“强占时段”,学生自由预约算作“弱占时段”。强占在学期初一次性导入,弱占则随时发起。两者共用同一条冲突检测逻辑,但优先级不同。这个拆分让我后来加功能时少改了很多代码。

2.2 核心预约表的设计思路

appointment表我并没有简单做成“谁在什么时间用了哪个实验室”,而是把时间拆成了date、start_period、end_period三个字段。start_period和end_period存的是第几节课,而不是具体的时间字符串。因为课表的调度单位就是节次,用节次做排课判断,SQL写起来干净,前端展示也直接对应课表格子。

表里还保留了status、source、semester_id三个关键字段。source区分这条记录是课程排课还是自主预约,展示优先级和后续权限判断都用它。status我在前面说了,是状态机字段。最后还有一个version字段,这是并发控制用的,下面讲冲突检测时会展开。

另外我还加了一个group_id字段,用来关联候补申请。学生提交候补时并不创建一条正式的appointment记录,而是在waiting_queue表里插入一条记录,表的字段包括appointment_id、student_id、apply_time。当有人取消预约时,系统扫描waiting_queue找到最早的未递补记录,自动创建一个已确认的appointment,并把原位置的预约状态改成已取消。

2.3 格言:宁可多建表,不要宽表硬撑

最初我为了图方便,把周次、星期、节次直接存成了JSON字符串,比如"1-16周"、"周一"、"3-4节"。查询时用like去匹配,开发期爽翻天,加了几个复杂查询之后彻底翻车。比如想查“周三第三四节哪个实验室空闲”,SQL里要拆JSON再匹配,性能差不说,逻辑还容易错。

后来我改成一张schedule_slot表,每条记录是一次排课或预约与某个具体时间片段的映射。字段包括appointment_id、week、weekday、start_period、end_period。这样做的好处是:一个持续八周的课程,在schedule_slot表里会有八条记录,每一条都能单独参与冲突检测。查询空闲实验室时,反查这张表看哪些时间段没有记录即可,逻辑非常直白。

3. 排课冲突检测算法:从暴力遍历到分段锁

3.1 首选方案:时间片段交叉判断

拿到一个排课请求后,后端要做两件事:判断这个实验室在请求的时间段内是否空闲,以及判断请求者本身是否在这个时间段已有安排。

时间重合的判断,我写了一个通用函数:给定已有排课的时间点(s1, e1)和待排课的时间点(s2, e2),两者冲突的条件是s1 < e2 and s2 < e1。这里所有时间都被映射成课节序号,天然避免了时区、日期格式的一堆问题。再加上lab_id过滤,一句SQL就能查出当前时段有没有别的排课记录:

conflict = session.query(ScheduleSlot).filter( ScheduleSlot.lab_id == lab_id, ScheduleSlot.week == week, ScheduleSlot.weekday == weekday, ScheduleSlot.start_period < req_end, ScheduleSlot.end_period > req_start, ScheduleSlot.is_cancelled == False ).first()

这段逻辑是整个系统的地基。后来接排课批量导入时,所有数据都先转换成ScheduleSlot临时对象,再逐条跑这个检测。撞上了就返回具体冲突的课程名,没撞上就批量落库。这里我会先把所有涉及到的节次按(week, weekday)分组,组内排序后做一次扫描,把复杂度控制在O(n log n),而不是在循环里反复查库。

3.2 并发预约的超卖问题:乐观看法的陷阱

最初上线时,我以为冲突检测已经挡住了重复预约,结果试运行第三天就出了事故。两个学生同时提交同一个实验室同一时段的预约,各自跑完SELECT都发现没有冲突,随后都插入成功,一间只能坐20人的实验室被约了两批人。

这个问题的本质是检查与插入之间没有原子性。解决方案有两个:一是把所有检查和插入放进数据库事务,再给ScheduleSlot表加唯一索引约束;二是引入乐观锁版本号,更新时校验version。我两个都做了。唯一索引是最硬的一道闸,版本号则用来处理同一用户重复提交流程的友好提示。

具体做法是给schedule_slot表创建一个组合唯一索引(lab_id, week, weekday, start_period, end_period),落库前用INSERT ... ON CONFLICT捕获冲突,一旦捕获就回滚事务,并返回“该时段已被占用”的提示。这样即使并发请求再多,数据库层面也能兜底。版本号字段则主要用在审核节点:管理员确认或驳回时,必须带着version更新,如果版本过期则提示刷新列表。

3.3 排课导入的批量处理:先算后写、失败隔离

批量导入课程表时,最怕遇到一半数据有问题导致全部回滚。我的处理方式是两步走:先把Excel所有行解析成内存中的排课条目,逐条做完整合法性校验(时间格式、教室是否存在、老师是否存在、是否冲突),把错误原因收集到列表里;如果错误数为0,才开始批量写入。如果错误数大于0,则整批不写入,直接返回一个错误报告。

这个“先算后写”的策略在实际使用中很受欢迎。以前管理员导入一张有400条记录的Excel,经常提示第217行有问题,改完重新导入又报第333行,来回折腾。现在一次把所有错误列出来,管理员一次性改完再导入,省了无数沟通成本。

4. 小程序端:从原型到能用的几个关键细节

4.1 页面结构:角色决定入口,入口决定路由

小程序端我做得比较克制,一共只做了5个主页面。首页就是课表视图,学生直接看到本周自己的预约;实验室列表页,根据容量、设备标签、开放状态筛选可用实验室;预约表单页,选择日期、节次、人数、用途描述;管理员页,包括实验室管理、课程导入、预约审核三个子功能;个人中心页,处理登录态和我的预约。

有一点我想特别提醒:小程序页面路由不要按功能去堆,要按角色去划分。一个页面如果同时服务学生和管理员,会很快陷入权限判断的泥潭。我把管理员功能全部收敛到单独的路由分组里,每个页面进入前统一做role校验,后端接口也同步校验,形成双保险。这样反而让代码更清晰,后续加功能时不需要动旧页面的逻辑。

4.2 请求封装与登录态:openid换token

登录流程上,我的最终版本是这样的:小程序端wx.login拿到临时code,发送到后端/api/auth/login,后端拿着code去微信接口换openid,再查自己的user表。如果用户已存在,直接签发一个JWT token返回;如果是新用户,则默认注册为一个学生账号,绑定openid后签发token。

这里有两个细节容易踩坑。第一个是request必须用wx.request的header带Authorization: Bearer token,后端Flask每次从Header里解析token。解析失败统一返回401,小程序端收到401就清空本地storage并跳登录页。不要把token放在URL query里,微信开发者工具里看不出问题,上线后日志里全是token记录,既乱又容易泄漏。

第二个是缓存策略。小程序冷启动时不要每次都调login接口,token有效期我设成了7天,本地存一份expires_at,没过期就直接用。如果过期了,调用静默登录接口刷新token,用户无感。只有静默登录也失败(比如用户主动清掉了微信授权)才强制走完整登录流程。

4.3 日期与节次的联动:前端组件农合的问题

小程序端的日期选择器和节次选择器如果独立使用没问题,一旦联动就特别容易出错。比如用户先选了“周一”,再选“第7-8节”,此时如果用户返回去改日期为“周三”,节次还停留在原来的选择上,就可能产生一条不合理的预约。

我引入了一个简单的状态重置逻辑:日期一旦变化,节次和实验室的所有已选内容全部清空,并用setData重新渲染。另外在提交前再做一次后端校验,即便前端漏了,后端依旧会拦住。这种“前端防呆+后端校验”的组合,在预约系统里是必备的。

页面里还有一个很小的体验优化:我根据用户选的时间去查可预约的实验室,返回结果里直接带上了每个实验室的“最近空闲时段”,用tags字段展示在卡片上。学生不用自己脑补哪间可以使用,选起来快很多。

5. 后端接口设计:面向状态的API,不面向页面

5.1 状态机驱动的接口流转

这系统的所有核心接口都是围绕预约状态机来设计的。学生端的接口只有四个:提交预约、取消预约、确认到场、查询列表。老师的接口多两个:发起排课、调停课。管理员则在此基础上多了审核通过、审核驳回、批量导入。

POST /api/appointment接收lab_id, date, start_period, end_period, purpose,后端先做时间校验和冲突检测,通过后创建待审核记录。POST /api/appointment/cancel只允许操作status为待审核或已确认的订单,且发起人必须是预约人本人。取消已确认的订单时,如果该订单在waiting_queue里已有候补者,会触发自动递补,递补结果通过订阅消息通知候补学生。POST /api/appointment/confirm是管理员把待审核改为已确认;改成已确认后,系统会发模板消息提醒学生。

我把接口语义全部收敛成动作,而不是更新字段。早期我也写过POST /api/appointment/update,传一个大JSON进去改任意字段,上线一周就出了好几次数据错乱。改成动作式接口后,每个接口只允许一个特定的状态迁移,问题一下少了很多。

5.2 查询接口的三种视角:时间、实验室、用户

查询接口我做了三个维度。GET /api/labs/available?date&start_period&end_period返回指定时间空闲的实验室列表;GET /api/timetable?week&role返回课表视图数据,按星期分列,每个单元格里填充预约单;GET /api/appointments?status返回当前用户的历史预约,角色不同返回的数据范围也不同。

课表视图这个接口一开始我写得很笨,直接查出所有课程,后端二维数组排好发给前端。后来数据量上来,我发现纯后端拼二维数组逻辑特别绕,改成了后端只吐扁平列表,每个元素带上weekday, start_period, duration,前端用wx:for嵌套循环生成表格。这样前端渲染逻辑清晰,后端也不需要在JSON里构建坐标系。

5.3 消息通知:模板消息的替代方案

wx.sendSubscribeMessage现在必须有用户的一次授权动作触发,不能像以前的模板消息那样任意推送。这个限制对预约系统影响不大,只要学生在提交预约时勾选“允许发送通知”,后续审核结果、候补递补、开课提醒都可以推送。麻烦的是授权一次只能发送一条消息,想发多条就要多次引导用户授权。

我的策略是只在关键节点推一条:审核结果。至于预约成功、取消成功这类信息,小程序内做一个红点提醒,用户进入小程序即可看到,不需要推送。校园场景里学生打开微信的频率极高,站内信式的提醒完全够用。这一点值得做预约类系统的人留意,不要在产品设计上过度依赖微信推送能力。

6. 上线前的自测清单与部署方案

6.1 自测清单:并发、越权、时间边界

预约类系统上线前,我建议至少过一遍这几个维度的测试。并发测试不用上Jemeter,Postman开两个tab同时点提交就能复现超卖问题;越权测试要专门试一下学生身份能否调管理员的审核接口、能否查别人的预约记录;时间边界测试则要覆盖跨天、跨周、学期最后一天、第16周等极端时间点。

其中时间边界最容易出bug。比如学期结束日期是第16周周五,但如果课程设置了“第16周周六补课”,一周是按周一到周日算还是周一到周五算,必须提前定好。我在后端维护了一个week_offset配置,统一规定一周从周一开始,但允许单条预约的weekday落在特殊补课日上,这样逻辑不会冲突。

6.2 部署:一台2C4G的小服务器足够

后端部署用了最常规的Nginx + uWSGI + Flask。服务器是2核4G的云主机,跑这套系统毫无压力,数据库刚迁移到MySQL的时候也没见CPU跑到过30%以上。SSL证书用免费版的,小程序request域名必须是HTTPS,这点没有商量的余地。

Nginx配置里我额外加了两条很重要的规则。第一条是client_max_body_size 10m,不然管理员导入Excel时超过默认1MB就会突然失败。第二条是把/api/路径的缓存设为no-cache,动态接口不能被CDN缓存,否则会出现数据看起来没更新的诡异问题。静态资源则能缓存就缓存,小程序本身打包了资源,这块影响不大。

6.3 小程序审核:隐私保护是重头

微信小程序审核被拒最多的情况是隐私协议不完整。提交审核前必须在小程序后台填写用户隐私保护指引,说明你收集了微信昵称、头像、手机号等哪些信息,并在代码里用wx.getPrivacySetting做前置检查。预约系统涉及学生实名信息,连实验室预约的时间记录都算敏感信息,所以在后端日志里我刻意不打username全名,统一用脱敏后的昵称。

另外审核环境里没有真实账号,最好提供几个测试账号,并在审核备注里写明测试流程。我在首次提审时没写清楚,被拒了一次,原因是审核员看不到如何登录系统。第二次我在备注里给了“一键体验”入口,审核员点进去就能以学生身份跑通预约流程,当天就过审了。

7. 回顾:这套系统的可复用设计模式

做完这个项目再往回看,真正高价值的不是某段代码,而是几个设计模式。排课冲突检测的时间片段交叉法,换个会议室预约系统也一样适用;乐观锁加唯一索引的并发控制,放到抢课、抢票系统里都是标准答案;状态机驱动的接口设计,让你永远不需要处理“订单既不是已确认又不是已取消”的中间状态。

我也总结了一点教训。最开始我陷入了功能堆叠的误区,想一次性把意见反馈、数据统计、消息通知全做进去,结果核心预约流程拖了两周才跑通。后来砍掉所有非必要功能,集中开发核心链路,系统才真正可用。做这种工具型系统,20%的核心功能满足80%的日常需求,剩下的只能让位给迭代计划。

如果现在有人要复刻这套实验室预约排课系统,我会建议他把精力优先放在这三个地方:冲突检测的正确性、并发控制的健壮性、权限校验的完整性。这三个点撑住了,系统哪怕界面朴素一点,体验也不会差。相反,如果花大量时间打磨样式和动画,核心逻辑却经不起一秒两个请求的冲击,上线只会到处救火。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 16:03:18

输电网规划实战:电压等级、容载比与变电站布点

简介&#xff1a;这份PPT系统地梳理了输电网规划与可靠性的核心知识体系&#xff0c;面向电力系统专业学生、电网规划工程师及科研人员&#xff0c;帮助读者掌握从输电方式选择、电压等级确定到变电站布局与网络结构设计的完整规划流程。资源为单个pptx课件文件&#xff0c;大小…

作者头像 李华
网站建设 2026/9/30 15:57:16

SAM3安装问题排查指南:环境配置、权重下载与微调依赖详解

搜索框里敲下“SAM3 安装问题”的人&#xff0c;大概率都已经在终端和报错之间来回拉扯了好几个小时。明明教程里的演示一切正常&#xff0c;自己照着敲&#xff0c;不是缺包就是版本冲突&#xff0c;要么权重下载到一半就断&#xff0c;好不容易全装好了又发现读不进模型。说实…

作者头像 李华