
微信小程序端的教师排课系统我刚好完整跟过一个类似项目从需求梳理到上线差不多折腾了一个多月。这个标题里藏的信息量其实不小前端用 uniapp 做跨端小程序端跑在微信上后端用 PHP 提供接口核心业务则是排课。很多人看到“排课系统”第一反应是“做个课表展示嘛”真做起来才发现课表展示只是冰山一角——排课规则、冲突检测、调课审批、周次解析、教师课时统计每一块都是坑。这篇就把我当时踩过的、想明白的、反复改过的细节全部摊开来讲希望能帮你少走点弯路。1. 整体设计与思路拆解1.1 需求定位这个系统到底在解决什么问题先别急着写代码想清楚业务边界比什么都重要。教师排课系统这类项目本质上是在解决三件事排课怎么排得合理、调课怎么走得顺畅、师生怎么看得清楚。我接触到的这个模拟项目X需求方是一所中职学校规模不算大大概有 80 多名教师、60 多个班级、300 多门课程。之前他们靠教务员手工在 Excel 里排课每次遇到教师请假、教室冲突、课程调整都要来回打电话确认光是秋季开学那一次排课就要折腾两周。所以他们要的不只是一个“电子课表”而是一套能把排课过程数字化、把冲突提前拦住、让教师通过手机就能看课表和处理调课申请的完整闭环。这个定位直接影响技术选型。为什么用微信小程序因为教师群体几乎人人都有微信不需要额外装 App扫码即用学习成本几乎为零。为什么前端用 uniapp因为学校领导后来提了一句“以后可能要在钉钉上用”用 uniapp 可以一套代码同时编译到微信小程序和 H5以后真要上钉钉工作量也不会太大。后端选 PHP 则比较务实学校的服务器通常是普通的虚拟主机或者小带宽云服务器PHP 部署简单、维护成本低而且这个量级的并发PHP 完全扛得住。1.2 角色划分与权限设计排课系统天然有三种角色权限模型必须一开始就定清楚教务管理员拥有最高权限负责课程计划录入、排课操作、调课审批、教师管理、班级管理、教室管理。这个角色在小程序端和管理后台都有操作入口。教师主要使用小程序端查看个人课表、提交调课申请、查看调课审批结果、查看课时统计。学生/班级按需查看班级课表。在这个项目里学生端没有专门做而是通过公开的班级课表查询入口实现属于轻量需求。在具体实现时我用的是基于角色的访问控制模型后端通过中间件统一拦截请求根据登录用户身份和接口权限表进行校验。不要把权限判断散落在各个控制器里后面维护起来会很想骂人。1.3 业务流程闭环头脑里有全流程图非常关键哪怕不画出来也要想清楚每一步的走向。这个项目的核心流程有五条基础数据维护管理员先录入学期、教师、班级、课程、教室这是排课的前提。排课计划设定管理员为每个班级制定开课计划明确要上哪些课、每周几学时。自动/手动排课系统根据规则尝试排课生成初步课表。排课结果可能不完美需要手动调整。调课审批教师提交调课申请管理员审批通过后课表更新。课表发布与查询小程序端展示个人课表、班级课表支持按周查询和上课提醒。这一步直接决定了后续数据库表结构和接口设计的大方向。数据模型不是凭空设计的而是业务流程倒推出来的。比如因为要有调课审批流程课表记录就必须有状态字段不能简单一条数据写到死。2. 核心细节解析与实操要点2.1 数据库设计排课系统的地基数据表不需要太复杂但每张表都有它存在的原因。我按这个项目最精简的落地方式来设计一共是六张核心表教师表 teacherid, name, title, phone, department_id, is_admin基于常见开发实践的补充职称字段 title 可以用于后续排课中的教师等级限制比如高级职称教师可以优先选择课程时段。班级表 class_infoid, name, grade, student_count, head_teacher_id, remark课程表 courseid, name, credit, hours_per_week, is_required, teacher_id教室表 classroomid, name, capacity, type, location教室类型 type 很重要机房、普通教室、多媒体教室排课约束条件完全不同。排课记录表 scheduleid, class_id, course_id, teacher_id, classroom_id, weekday, section_start, section_count, week_start, week_end, status这张表是整个系统的核心排课记录实际上就是“谁在什么时间在哪个教室给哪个班级上什么课”的五元组。字段拆开解释weekday星期一到星期五取 1 到 7 整数。section_start 和 section_count起始节次和持续节数比如上午第 1、2 节连上section_start 就是 1section_count 是 2。week_start 和 week_end第几周到第几周上课这里支持灵活的周次范围而不是整个学期固定。比如某门实训课只在第 1 到第 8 周开就能精确表达。status1 正常、2 待调课、3 已调课。有了这个字段调课审批才做得出来撤销和追溯也都有据可查。调课申请表 course_adjustid, schedule_id, from_weekday, from_section, to_weekday, to_section, teacher_id, reason, status, create_time为什么不做逻辑删除而是加状态字段因为调课记录需要留痕管理员要能看到“这门课原本安排在哪、后来调到哪、谁申请、什么理由”所以用状态流转代替物理删除。2.2 排课冲突检测核心中的核心排课系统能不能用就看冲突检测做得到不到位。我把冲突分成四种冲突类型说明检测规则教师冲突同一教师同一时间在多个教室上课按 teacher_id weekday 时间段 周次检查重叠班级冲突同一班级同一时间安排两门课按 class_id weekday 时间段 周次检查重叠教室冲突同一教室同一时间被两个课程占用按 classroom_id weekday 时间段 周次检查重叠学时冲突课程周学时超过该班级周课时上限按班级每周总课时统计冲突检测的 SQL 写起来并不复杂核心思想是时间段重叠判断。两个时间段有重叠的条件是new_start old_end AND new_end old_start。排课表里我已经存了 section_start 和 section_count所以结束节次就是section_start section_count - 1。一段比较实用的检测 SQL基于常见实践的写法具体字段以你的设计为准SELECT COUNT(*) FROM schedule WHERE teacher_id :teacher_id AND status 1 AND weekday :weekday AND section_start :section_end AND (:section_start section_start section_count) AND week_start :week_end AND week_end :week_start如果查询结果大于 0就说明教师已经有课不能安排。班级冲突和教室冲突同理换成对应的 id 字段即可。2.3 周次解析一个容易被低估的设计点学校上课往往不是每学期从头到尾都排满比如第一周是入学教育不开课、专业技能课程在第 9 周到第 16 周才开又比如单双周交替上课的公共体育课。这些需求用一个“起止周”字段确实能覆盖一部分但遇到单双周就抓瞎了。所以在 schedule 表上还可以加一个字段处理这种复杂周模式我当时是加了一个week_type字段取值all、odd、even三个枚举值分别代表每周都上、单周上、双周上。查询课表时需要根据当前周次判断这节课是否生效判断逻辑通常是基于常见实践的补充if ($week_type all || ($week_type odd $current_week % 2 1) || ($week_type even $current_week % 2 0)) { // 本节课本周生效 }这个设计在实际使用中反馈很好教务员再也不用为单双周课单独做手工标注了。3. 实操过程与核心环节实现3.1 后端接口规划与 PHP 分层后端采用 T 字头框架比较常见这类框架的 MVC 分层清晰、文档多、上手快。接口设计遵循统一的返回格式{ code: 0, msg: success, data: {} }按模块划分接口大概有这些登录模块微信登录 code2session、token 签发教师模块教师信息、课表查询、调课申请管理模块班级管理、课程管理、教室管理排课模块手动排课、自动排课、冲突检测、课表发布调课模块调课审批、调课记录类设计上我习惯把 Service 层和 Controller 层分开。Controller 只做参数接收和返回格式化业务逻辑下沉到 Service 层。比如排课这种涉及多条表写入的操作如果在 Controller 里处理事务控制就会很痛苦。在 Service 层里可以这样处理public function createSchedule($data) { try { Db::startTrans(); // 冲突检测 $conflict $this-checkConflict($data); if ($conflict) { throw new \Exception(排课冲突); } // 写入排课记录 $scheduleId Db::name(schedule)-insertGetId($data); // 其他关联操作 Db::commit(); return $scheduleId; } catch (\Exception $e) { Db::rollback(); throw $e; } }3.2 前端 uniapp 页面结构与核心组件uniapp 项目的目录结构我会拆成这样pages/ ├── login/login.vue ├── index/index.vue // 首页展示本周课表 ├── schedule/schedule.vue // 完整课表页面支持周切换 ├── adjust/adjust.vue // 调课申请 ├── mine/mine.vue // 个人中心 └── admin/ ├── admin-schedule.vue // 管理端排课操作页 └── admin-approve.vue // 调课审批页课表展示可以说是小程序端最难做好的部分。微信小程序原生组件有限纯手写一套按周维度的课表网格需要处理跨行合并、课程颜色区分、点击跳转等交互。我推荐的做法是利用 uniapp 的scroll-view组件配合css grid实现横向以星期为列纵向以节次为行。每个课程块绝对定位在对应格子里。相互冲突的课程用不同背景色区分。这套方案在 iPhone 和安卓手机上的实测表现都不错触摸滚动流畅度比手写计算 scroll-left 的方式好得多。课表网格中课程块的数据结构整理成一个二维数组是标准做法// 按 weekday 分组后的课表数据 scheduleMap[weekday] [ { id: 101, courseName: PHP程序设计, teacherName: 张老师, classroom: 机房A301, sectionStart: 1, sectionCount: 2, weekStart: 1, weekEnd: 18, color: #4A90D9 } ]有了这个数据结构前端渲染就很简单了只需要遍历对应星期的数组根据 sectionStart 和 sectionCount 计算块的 top 和 height。这个方案我实测下来很稳维护也方便。3.3 排课算法贪心策略与手动干预排课系统要不要做自动排课是我和需求方争论最久的问题。教务员的诉求很简单“能一键生成课表最好”但教师和管理员的实际约束条件实在太多某些老师周一到周三不能排课、下午第一节尽量不排体育课、实验课必须连排两节以上……这些规则写进程序完全可能但自动生成出来的课表往往需要大量手动修正反而是不够聪明的自动排课比纯手动更费时间。最后定的方案是混合排课管理员先设定硬性约束教师可用时间、班级可用时间、课程连排要求。系统按“班级优先逐门课程匹配可用时间”的贪心策略生成初稿。管理员在可视化界面上手动拖拽调整。贪心匹配的核心逻辑其实不复杂伪代码示意foreach 班级 as c: foreach 该班级课程 as course: foreach 所有 weekday as d: foreach 所有时间段 as time: if 教师有课: continue if 班级有课: continue if 教室被占: continue 排课成功标记冲突记录真要做细致还有一个优先级排序问题周学时多的课程先排有特殊要求的课程先排教师时间受限多的先排。基于我在实际项目中的体会这个优先级顺序是自动排课满意度的关键参数没有绝对公式需要在具体业务里反复调优。3.4 调课审批与课表状态流转教师端发起调课涉及的操作有选择原课程记录、选择目标时间和目标教室、填写调课原因。管理员端收到申请后可以看到冲突检测结果——目标时间是否与该教师其他课程冲突、目标教室是否被占用检测通过后确认调课。状态流转用一张表描述很直观状态值含义可执行操作1正常排课教师可发起调课2调课待审批管理员审批通过/驳回3调课已通过原课程展示灰化新课程生效4调课已驳回原课程保持正常调课审批通过后后端需要做两步操作原 schedule 记录状态标记为 3 表示已调走然后插入一条新的 schedule 记录status 为 1。这个更新过程必须放在一个数据库事务里否则可能出现调课成功但新课表没生成的情况。3.5 课时统计的 SQL 实现课时统计是教务员月底必用的功能。每个教师的课时 所有有效排课记录的section_count之和但有一个细节容易遗漏跨周次的课程要乘以周数。以某教师第 1 周到第 16 周每周上 2 节为例总课时是 2 × 16 32。统计 SQL 大致是SELECT teacher_id, SUM(section_count * (week_end - week_start 1)) AS total_hours FROM schedule WHERE status 1 GROUP BY teacher_id;如果有单双周限制则课程实际周数要按单双周计算不是简单的周数相减加一。我在做这个功能的时候就是在统计函数里传入了week_type参数分别计算switch ($week_type) { case all: $weeks $weekEnd - $weekStart 1; break; case odd: $weeks countOddWeeks($weekStart, $weekEnd); break; case even: $weeks countEvenWeeks($weekStart, $weekEnd); break; }这个模块在交付后基本没有收到过核算错误的反馈之前手工统计要花半天的活现在几秒钟就出来。4. 常见问题与排查技巧实录4.1 日历组件跨月显示不对你是说小程序课表组件在跨月时显示这个坑我碰到过。课程表显示按周切换需要把“本周是教学周第几周”换算成日期。很多第一次写的同学会直接在 JavaScript 里算当前日期是学期的第几周结果遇到国庆、中秋放假调休就全部乱套。正确做法是以教务日历表为准后台维护一个学期信息表semester: { start_date: 2025-09-01, week_count: 20 }每周的起始日期动态计算程序不自行推理放假周次而是以管理员手动维护为准。国庆调休那几天管理员直接在后台把对应工作周做特殊标记即可前端只负责展示。4.2 微信登录 session 过期处理教师可能连续几周不打开小程序微信 code2session 拿到的 openid 没问题但自定义登录态 token 过期用户会突然发现自己被踢下线。解决方案是双 token 策略短期 access_token 有效期 2 小时长期 refresh_token 有效期 30 天。调用接口时如果 access_token 过期小程序端拿 refresh_token 静默换新整个过程用户无感知。refresh_token 也过期就跳转登录页。4.3 PHP 并发冲突问题两个管理员同时排同一个时间段的课程后提交的覆盖了先提交的这种情况听上去概率低但在开学排课集中期是真实存在的。解决思路是乐观锁schedule 表加一个 version 字段。排课前先读取 version。更新时带条件WHERE id ? AND version ?如果影响行数为 0说明记录已被别人改过需要重新读取再确认。这个方案代码改动小、效果直观不多占用数据库锁资源。我在模拟项目X里上线了乐观锁之后再没有出现过覆盖排课的事故。4.4 小程序 setData 大数据卡顿一个班级一周的课表数据如果是全量 setData大概也就 20 条记录完全不会卡顿。但如果做的是全校教师管理页一次性加载 100 条以上数据渲染列表setData 的性能瓶颈就会明显暴露。常规优化手段是分页加载。小程序端上拉触底时请求下一页数据每次请求 20 条。这个方案实现成本低稳定可靠。另一个可以尝试的手段是改用virtual-list虚拟列表但对于排课系统这种规模分页已经完全够用了。4.5 常见问题速查表现象可能原因解决办法登录后没有课表数据教师没有排课记录确认教师与课程已关联课程周次显示混乱周次字段传错week_start 大于 week_end后端参数校验调课审批通过但课表没变新记录未写入或事务未提交检查 schedule 表状态字段微信开发者工具正常真机空白域名未配置合法域名或未开启 HTTPS配置服务器域名课时统计偏少没有纳入调课后新记录统计时包含 status 为 1 的记录排除调课旧记录4.6 服务器部署的隐藏坑PHP 部署这块有几个容易被忽略但很关键的点。版本兼容性问题我开发机用的是 PHP 7.4学校服务器上装的是 PHP 5.6结果一堆语法兼容问题。开发前务必先确认生产环境版本或者直接在 Docker 里跑一个与生产环境一致的镜像。这个教训我印象很深后来所有交付项目第一件事就是统一环境。HTTPS 证书问题微信小程序要求所有接口请求必须是 HTTPS且域名需要在小程序后台配置。很多学校用的是阿里云、腾讯云的免费证书配置起来倒是不难但证书过期时间要记牢过期后小程序会直接请求失败这是运维中比较容易忽略的细节。4.7 数据安全问题排课系统涉及的教师信息、班级信息虽然不算高度敏感但也是学校内部数据不能裸奔。我会把密码字段用 bcrypt 哈希存储接口都走 HTTPS管理员后台做登录 IP 白名单限制。数据库备份采用定时任务每天凌晨全量备份到异地存储。学校场景下服务器出故障比较少但真出问题时如果没有备份后果是很麻烦的。这套配置我在模拟项目X里完整实施过算是项目交付的基本素养。5. 微信小程序端的发布与体验优化5.1 小程序端打开速度优化小程序包的体积限制是 2MB但实际控制在 1MB 以内体验会更好。uniapp 的tree-shaking只能去掉部分无用代码关键还是要靠分包加载。把管理员相关的页面全部放到subpackage分包里教师端常规页面保持很小的首包体积这样首次打开速度会明显更快。接口层面的优化也是大头。课表查询这种高频接口数据变化不频繁接口加一层 Redis 缓存非常划算$key schedule:teacher: . $teacherId . :week: . $week; $data Redis::get($key); if (!$data) { $data $this-getTeacherSchedule($teacherId, $week); Redis::set($key, json_encode($data), 600); // 缓存10分钟 }10 分钟过期时间足够保证数据及时性又不会频繁查数据库。在实际压测中这个缓存策略让课表接口响应时间从 300ms 降到了 30ms 左右。5.2 课表可视化体验提升课表页面是教师每天都要看的东西体验不好会被天天吐槽。我做了三个小优化反馈都非常好第一课程块颜色根据课程分类自动生成。比如理论课统一蓝色系、实训课绿色系、体育课橙色系教师扫一眼就能分辨课程类型。第二当前日期高亮今天这一列用淡黄色背景标记。教师打开小程序第一时间就看到“今天有什么课”。第三节次时间显示。很多新来的教师不知道第几节是几点上课我在节次栏上做了二级标题显示每节课的具体时间区间比如“第一节 08:00 - 08:45”。数据来源是后台配置的时间模板这样遇到学校调整作息时间后台改一次配置所有教师端同步更新。5.3 消息通知与提醒调课审批结果能不能主动推给教师这是需求方在项目后期加的需求。小程序不支持不打开就任意推送但可以通过订阅消息实现“一次性消息订阅”教师在提交调课申请时引导点击“允许发送审批结果通知”审批完成后后端通过模板消息推送结果。这个功能要做但没有太多经验可参考。uniapp 端调用uni.requestSubscribeMessage接口后端调微信接口发送模板消息签名用的是教师登录时的 access_token。注意模板消息有模板 ID 要求需要在小程序后台申请对应的模板其他没有太多要注意的。5.4 跨端兼容的实测心得我实际测试过 H5 和微信小程序端并行跑的情况。uniapp 的编译机制在处理 vue 文件时绝大多数代码两端通用。真正需要做条件编译的是登录逻辑——微信小程序用uni.login获取 codeH5 端用账号密码登录两者逻辑差异还是很大的。处理方式是在common/login.js里封装一个统一的登录方法内部用条件编译区分平台// #ifdef MP-WEIXIN const loginRes await uni.login(); const code loginRes.code; // #endif // #ifdef H5 const code null; // #endif这个方案在模拟项目X里跑得很顺后续如果要接钉钉端同一套代码也能直接编译适配。总的来说这个教师排课系统从需求到落地最值钱的部分不在 UI 多好看、代码多花哨而在于业务流程是否走通、冲突检测是否可靠、调课审批是否顺畅。我记得项目上线当天教务员看着自动生成的课表说了句“这比我手工排快多了”就这一句话前面熬的夜都值了。这套项目的完整思路和代码方案拿来给其他同事做同类需求参考完全没问题代码不是一次性买卖能复用才是真省事。