ARTICLE DETAIL

资讯详情

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

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

实验室预约排课系统设计: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?datestart_periodend_period返回指定时间空闲的实验室列表GET /api/timetable?weekrole返回课表视图数据按星期分列每个单元格里填充预约单GET /api/appointments?status返回当前用户的历史预约角色不同返回的数据范围也不同。课表视图这个接口一开始我写得很笨直接查出所有课程后端二维数组排好发给前端。后来数据量上来我发现纯后端拼二维数组逻辑特别绕改成了后端只吐扁平列表每个元素带上weekday, start_period, duration前端用wx:for嵌套循环生成表格。这样前端渲染逻辑清晰后端也不需要在JSON里构建坐标系。5.3 消息通知模板消息的替代方案wx.sendSubscribeMessage现在必须有用户的一次授权动作触发不能像以前的模板消息那样任意推送。这个限制对预约系统影响不大只要学生在提交预约时勾选“允许发送通知”后续审核结果、候补递补、开课提醒都可以推送。麻烦的是授权一次只能发送一条消息想发多条就要多次引导用户授权。我的策略是只在关键节点推一条审核结果。至于预约成功、取消成功这类信息小程序内做一个红点提醒用户进入小程序即可看到不需要推送。校园场景里学生打开微信的频率极高站内信式的提醒完全够用。这一点值得做预约类系统的人留意不要在产品设计上过度依赖微信推送能力。6. 上线前的自测清单与部署方案6.1 自测清单并发、越权、时间边界预约类系统上线前我建议至少过一遍这几个维度的测试。并发测试不用上JemeterPostman开两个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%的日常需求剩下的只能让位给迭代计划。如果现在有人要复刻这套实验室预约排课系统我会建议他把精力优先放在这三个地方冲突检测的正确性、并发控制的健壮性、权限校验的完整性。这三个点撑住了系统哪怕界面朴素一点体验也不会差。相反如果花大量时间打磨样式和动画核心逻辑却经不起一秒两个请求的冲击上线只会到处救火。
返回列表