ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微信小程序教师排课系统实战:基于uniapp+PHP的完整设计与实现

微信小程序教师排课系统实战:基于uniapp+PHP的完整设计与实现

微信小程序端的教师排课系统,我刚好完整跟过一个类似项目,从需求梳理到上线差不多折腾了一个多月。这个标题里藏的信息量其实不小:前端用 uniapp 做跨端,小程序端跑在微信上,后端用 PHP 提供接口,核心业务则是排课。很多人看到“排课系统”第一反应是“做个课表展示嘛”,真做起来才发现,课表展示只是冰山一角——排课规则、冲突检测、调课审批、周次解析、教师课时统计,每一块都是坑。这篇就把我当时踩过的、想明白的、反复改过的细节全部摊开来讲,希望能帮你少走点弯路。

1. 整体设计与思路拆解

1.1 需求定位:这个系统到底在解决什么问题

先别急着写代码,想清楚业务边界比什么都重要。教师排课系统这类项目,本质上是在解决三件事:排课怎么排得合理、调课怎么走得顺畅、师生怎么看得清楚。

我接触到的这个模拟项目X,需求方是一所中职学校,规模不算大,大概有 80 多名教师、60 多个班级、300 多门课程。之前他们靠教务员手工在 Excel 里排课,每次遇到教师请假、教室冲突、课程调整,都要来回打电话确认,光是秋季开学那一次排课就要折腾两周。所以他们要的不只是一个“电子课表”,而是一套能把排课过程数字化、把冲突提前拦住、让教师通过手机就能看课表和处理调课申请的完整闭环。

这个定位直接影响技术选型。为什么用微信小程序?因为教师群体几乎人人都有微信,不需要额外装 App,扫码即用,学习成本几乎为零。为什么前端用 uniapp?因为学校领导后来提了一句“以后可能要在钉钉上用”,用 uniapp 可以一套代码同时编译到微信小程序和 H5,以后真要上钉钉,工作量也不会太大。后端选 PHP 则比较务实,学校的服务器通常是普通的虚拟主机或者小带宽云服务器,PHP 部署简单、维护成本低,而且这个量级的并发,PHP 完全扛得住。

1.2 角色划分与权限设计

排课系统天然有三种角色,权限模型必须一开始就定清楚:

  • 教务管理员:拥有最高权限,负责课程计划录入、排课操作、调课审批、教师管理、班级管理、教室管理。这个角色在小程序端和管理后台都有操作入口。
  • 教师:主要使用小程序端,查看个人课表、提交调课申请、查看调课审批结果、查看课时统计。
  • 学生/班级:按需查看班级课表。在这个项目里,学生端没有专门做,而是通过公开的班级课表查询入口实现,属于轻量需求。

在具体实现时,我用的是基于角色的访问控制模型,后端通过中间件统一拦截请求,根据登录用户身份和接口权限表进行校验。不要把权限判断散落在各个控制器里,后面维护起来会很想骂人。

1.3 业务流程闭环

头脑里有全流程图非常关键,哪怕不画出来也要想清楚每一步的走向。这个项目的核心流程有五条:

  1. 基础数据维护:管理员先录入学期、教师、班级、课程、教室,这是排课的前提。
  2. 排课计划设定:管理员为每个班级制定开课计划,明确要上哪些课、每周几学时。
  3. 自动/手动排课:系统根据规则尝试排课,生成初步课表。排课结果可能不完美,需要手动调整。
  4. 调课审批:教师提交调课申请,管理员审批通过后,课表更新。
  5. 课表发布与查询:小程序端展示个人课表、班级课表,支持按周查询和上课提醒。

这一步直接决定了后续数据库表结构和接口设计的大方向。数据模型不是凭空设计的,而是业务流程倒推出来的。比如因为要有调课审批流程,课表记录就必须有状态字段,不能简单一条数据写到死。

2. 核心细节解析与实操要点

2.1 数据库设计:排课系统的地基

数据表不需要太复杂,但每张表都有它存在的原因。我按这个项目最精简的落地方式来设计,一共是六张核心表:

教师表 teacher

id, name, title, phone, department_id, is_admin

基于常见开发实践的补充:职称字段 title 可以用于后续排课中的教师等级限制,比如高级职称教师可以优先选择课程时段。

班级表 class_info

id, name, grade, student_count, head_teacher_id, remark

课程表 course

id, name, credit, hours_per_week, is_required, teacher_id

教室表 classroom

id, name, capacity, type, location

教室类型 type 很重要,机房、普通教室、多媒体教室,排课约束条件完全不同。

排课记录表 schedule

id, 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 就是 1,section_count 是 2。
  • week_start 和 week_end:第几周到第几周上课,这里支持灵活的周次范围,而不是整个学期固定。比如某门实训课只在第 1 到第 8 周开,就能精确表达。
  • status:1 正常、2 待调课、3 已调课。有了这个字段,调课审批才做得出来,撤销和追溯也都有据可查。

调课申请表 course_adjust

id, 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 排课算法:贪心策略与手动干预

排课系统要不要做自动排课,是我和需求方争论最久的问题。教务员的诉求很简单:“能一键生成课表最好”,但教师和管理员的实际约束条件实在太多:某些老师周一到周三不能排课、下午第一节尽量不排体育课、实验课必须连排两节以上……这些规则写进程序完全可能,但自动生成出来的课表往往需要大量手动修正,反而是不够聪明的自动排课比纯手动更费时间。

最后定的方案是混合排课:

  1. 管理员先设定硬性约束:教师可用时间、班级可用时间、课程连排要求。
  2. 系统按“班级优先,逐门课程匹配可用时间”的贪心策略生成初稿。
  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获取 code,H5 端用账号密码登录,两者逻辑差异还是很大的。

处理方式是在common/login.js里封装一个统一的登录方法,内部用条件编译区分平台:

// #ifdef MP-WEIXIN const loginRes = await uni.login(); const code = loginRes.code; // #endif // #ifdef H5 const code = null; // #endif

这个方案在模拟项目X里跑得很顺,后续如果要接钉钉端,同一套代码也能直接编译适配。总的来说,这个教师排课系统从需求到落地,最值钱的部分不在 UI 多好看、代码多花哨,而在于业务流程是否走通、冲突检测是否可靠、调课审批是否顺畅。我记得项目上线当天,教务员看着自动生成的课表说了句“这比我手工排快多了”,就这一句话,前面熬的夜都值了。这套项目的完整思路和代码方案,拿来给其他同事做同类需求参考完全没问题,代码不是一次性买卖,能复用才是真省事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进