微信小程序+PHP+uniapp教师排课系统开发实战
发布时间:2026/9/18 21:48:50 作者:尧图编辑部 阅读量:1,286

如果你正在准备毕业设计或者接手了一个给学校、培训机构做排课系统的项目那今天这篇内容应该能帮到你。聊的是一个非常典型的组合微信小程序 PHP uniapp 的教师排课系统。标题后面那串“rv98tluz”其实是不少开发工具自动生成的项目标识不用管它。这种组合为什么在校园类项目里这么流行简单说微信小程序解决了“学生/老师不用装App扫码就能看课表”的入口问题PHP负责后端数据管理和接口输出成熟稳定、资料多uniapp则让前端代码可以一套跑多端——小程序、H5、甚至App都能覆盖。对独立开发者或者学生团队来说这是目前性价比最高、踩坑成本最低的选型之一。整篇文章我不会只讲功能列表而是按一个可复现的完整项目来拆从系统设计、数据库表结构、PHP接口实现到小程序端课表渲染、调课逻辑、冲突检测再到联调阶段容易踩的坑。每个环节我都会写到具体步骤和代码片段而不是泛泛而谈。1. 系统整体设计与架构拆解1.1 角色与核心业务流教师排课系统表面上是“排课”但落到真实场景里至少要处理三类用户的需求教务管理员维护班级、教师、教室、课程基础数据负责排课、调课、发布课表。教师查看个人课表、确认授课安排、提交调课申请。学生查看班级课表和教室课表了解上课时间和地点。从业务流程上看排课并不是“一行数据加进去就完事”它背后有一条完整链路基础数据维护 → 排课约束检查 → 时间片冲突检测 → 生成课表 → 课表发布 → 调课申请与审批。很多初做排课系统的人容易只盯着“增删改查”忽略冲突检测和调课流程结果做完的只是一个课程管理后台不是排课系统。1.2 为什么选微信小程序 PHP uniapp这个组合能成为校园类项目的“标准答案”有它现实层面的合理性微信小程序做C端大学校园里微信覆盖率几乎100%学生、老师不需要额外装App小程序“搜一下”或者“扫一下”就能用。相比开发原生Android/iOS小程序的审核和分发流程也更适合校园场景。PHP做服务端PHP在Web服务端的开发效率非常高尤其是配合ThinkPHP这种成熟框架路由、ORM、验证器、中间件都很齐全适合一个人快速完成后端开发。部署也简单虚拟主机或者一台普通云服务器就能跑起来。uniapp做跨端uniapp最大的价值是“一次编写多端运行”。同样是排课系统前端uniapp可以编译到微信小程序也可以编译成H5挂在公众号里还能打App包。虽然每个端多少有些兼容性小问题但对资源有限的团队来说这个取舍非常划算。1.3 项目目录结构与前端基础架构实际项目中我习惯把代码分成两个工程目录server放PHP后端client放uniapp前端。teacher-schedule-system/ ├── server/ # PHP 后端ThinkPHP 6 │ ├── app/ │ │ ├── controller/ # 接口控制器 │ │ ├── model/ # 数据模型 │ │ └── middleware/ # 鉴权中间件 │ ├── config/ # 数据库等配置 │ └── route/ # 路由定义 └── client/ # uniapp 前端 ├── pages/ │ ├── login/ # 登录页 │ ├── index/ # 首页/本周课表 │ ├── schedule/ # 课表详情 │ └── mine/ # 个人中心 ├── api/ # 接口请求封装 ├── components/ # 通用组件 └── utils/ # 日期处理等工具函数前端页面虽然看着模块不多但课表页是整个系统的核心交互界面它的渲染逻辑和数据结构设计直接相关后面我会专门展开讲。2. 数据库设计排课系统的地基2.1 核心数据表结构数据库设计是整个系统里最不能赶工的部分。表结构如果设计不合理后面写接口时你会非常痛苦。排课系统有几张核心表几乎是必须的。教师表teacher字段名类型说明idint主键teacher_novarchar(20)教师工号namevarchar(50)姓名phonevarchar(20)手机号titlevarchar(50)职称可选create_timedatetime创建时间班级表class字段名类型说明idint主键class_novarchar(20)班级编号class_namevarchar(100)班级名称gradevarchar(10)年级student_countint人数排课容量参考教室表classroom字段名类型说明idint主键room_novarchar(20)教室编号buildingvarchar(50)所在楼栋capacityint容量typevarchar(20)类型普通教室/机房/实验室课程表course字段名类型说明idint主键course_novarchar(20)课程编号course_namevarchar(100)课程名称creditdecimal(3,1)学分hoursint总学时排课记录表schedule_record这张表是整个系统的核心每一行代表“某班级在某时间、某教室由某老师上某门课”。字段名类型说明idint主键termvarchar(20)学期如2024-2025-1teacher_idint教师IDclass_idint班级IDcourse_idint课程IDclassroom_idint教室IDweek_daytinyint星期几1-7start_sectiontinyint开始节次end_sectiontinyint结束节次start_weektinyint开始周次end_weektinyint结束周次statustinyint状态1发布 0草稿 2已调课为什么week_day要用数字而不是直接用“星期一”这种字符串因为数字在判断星期几的先后顺序、做索引查询时都更高效展示层再映射成中文就行。start_section和end_section表示节次范围比如第1节到第2节。这样设计的好处是支持一次排多节课也方便做冲突检测。2.2 冲突检测的字段设计逻辑排课系统最核心的功能不是“录入课表”而是“防止冲突”。一个教室同一时间只能上一门课一个老师同一时间也只能教一门课这两条规则必须在数据库层面就能快速查到。我在设计时schedule_record表建了两个联合索引ALTER TABLE schedule_record ADD INDEX idx_room_time (classroom_id, week_day, start_section, end_section); ALTER TABLE schedule_record ADD INDEX idx_teacher_time (teacher_id, week_day, start_section, end_section); ALTER TABLE schedule_record ADD INDEX idx_class_time (class_id, week_day, start_section, end_section);这样在做冲突检测时查询条件可以直接命中索引即便后续数据到几万行也不会有明显的性能问题。时间片冲突的判断逻辑我的做法是把“节次”看成一维数轴上的区间两个排课冲突的条件是星期几相同周次范围有交集节次区间有交集即start_section 已有记录的end_section且end_section 已有记录的start_section比如已有记录是第2节到第4节新记录如果排第3节到第5节那就是冲突排第1节到第2节没冲突排第5节之后也没冲突。2.3 调课申请表schedule_adjust排课系统里调课是高频业务。老师临时有事、教室被占用、学校临时安排活动都需要调课。调课申请流程如果做成“线下申请、人工改表”系统价值就会大打折扣。我设计了一张调课申请表字段名类型说明idint主键schedule_idint原排课记录IDapply_teacher_idint申请人old_week_daytinyint原星期old_start_sectiontinyint原开始节次old_end_sectiontinyint原结束节次new_week_daytinyint目标星期new_start_sectiontinyint目标开始节次new_end_sectiontinyint目标结束节次reasonvarchar(255)调课原因statustinyint0待审批 1同意 2拒绝create_timedatetime申请时间这样设计的目的是保留“原课表信息快照”申请人和审批人一眼就能看清从什么时间调到什么时间避免“改完就忘”的问题。3. PHP服务端接口设计与实现3.1 框架选型与环境准备后端我用的是ThinkPHP 6主要是因为它结构清晰、中文文档齐全、适合快速开发中小型系统。环境方面本地用PHPStudy一键搭建Nginx PHP 8.1 MySQL 5.7生产环境用宝塔面板部署非常简单。创建项目composer create-project topthink/think tp然后在.env里配置数据库连接[DATABASE] TYPE mysql HOSTNAME 127.0.0.1 DATABASE schedule_db USERNAME root PASSWORD your_password HOSTPORT 3306 CHARSET utf8mb4 PREFIX tp_3.2 登录与鉴权实现小程序的登录流程跟网页登录不太一样它的核心是wx.login拿到临时code然后把code传给后端后端调用微信接口换取openid再生成自定义登录态。网上很多资料把这一步叫“code换token”本质就是这个流程。PHP端接收code的接口public function login(Request $request) { $code $request-param(code); if (empty($code)) { return json([code 1, msg 参数错误]); } // 调用微信接口换取 openid $appid config(wechat.appid); $secret config(wechat.secret); $url https://api.weixin.qq.com/sns/jscode2session?appid{$appid}secret{$secret}js_code{$code}grant_typeauthorization_code; $result file_get_contents($url); $data json_decode($result, true); if (isset($data[openid])) { // 根据 openid 查用户不存在则自动注册游客身份 $user UserModel::where(openid, $data[openid])-find(); if (!$user) { $user UserModel::create([ openid $data[openid], nickname 微信用户, role student ]); } // 生成 token实际项目建议用 JWT 或 think-token $token md5($user[id] . time() . rand(1000, 9999)); cache(token_ . $token, $user[id], 3600 * 24 * 7); return json([ code 0, msg ok, data [token $token, user_info $user] ]); } return json([code 1, msg 登录失败]); }注意生产环境千万不要直接用file_get_contents调微信接口这只是示例代码。建议用 Guzzle HttpClient并且要做超时和异常处理否则接口响应慢了小程序端会一直转圈。3.3 排课接口与冲突检测逻辑排课保存是整个系统的核心接口。管理员在前端选好老师、班级、课程、教室、时间后后端在写入数据库前必须做冲突校验。public function saveSchedule(Request $request) { $data $request-param(); $validate new Validate([ teacher_id require|number, class_id require|number, course_id require|number, classroom_id require|number, week_day require|between:1,7, start_section require|number|between:1,12, end_section require|number|between:1,12, start_week require|number, end_week require|number, ]); if (!$validate-check($data)) { return json([code 1, msg $validate-getError()]); } if ($data[start_section] $data[end_section]) { return json([code 1, msg 开始节次不能大于结束节次]); } // 冲突检测 $conflict $this-checkConflict($data); if ($conflict) { return json([code 1, msg 时间冲突 . $conflict]); } $record ScheduleModel::create($data); return json([code 0, msg 排课成功, data $record]); }冲突检测方法private function checkConflict($data) { // 教室冲突 $roomConflict ScheduleModel::where(classroom_id, $data[classroom_id]) -where(week_day, $data[week_day]) -where(start_section, , $data[end_section]) -where(end_section, , $data[start_section]) -where(status, , 2) -find(); if ($roomConflict) { return 该教室在此时段已被占用; } // 教师冲突 $teacherConflict ScheduleModel::where(teacher_id, $data[teacher_id]) -where(week_day, $data[week_day]) -where(start_section, , $data[end_section]) -where(end_section, , $data[start_section]) -where(status, , 2) -find(); if ($teacherConflict) { return 该教师在此时间段已有课程; } // 班级冲突 $classConflict ScheduleModel::where(class_id, $data[class_id]) -where(week_day, $data[week_day]) -where(start_section, , $data[end_section]) -where(end_section, , $data[start_section]) -where(status, , 2) -find(); if ($classConflict) { return 该班级在此时段已有课程; } return false; }这里我把教室、教师、班级三个维度的冲突分开查目的是返回更明确的错误信息方便管理员快速定位问题。有人可能会说“用一条SQL查出来就行”但对使用者来说“是教室冲突还是老师冲突”完全是两种处理方式分开提示体验更好。3.4 课表查询接口课表查询是前端调用频率最高的接口。这里我会根据角色区分返回逻辑学生只看班级课表教师看个人课表教务管理员可以按条件组合筛选。以班级课表接口为例public function getClassSchedule($classId, $term ) { $term $term ?: date(Y) . - . (date(Y) 1) . -1; $list ScheduleModel::where(class_id, $classId) -where(term, $term) -with([teacher, course, classroom]) -select(); return json([code 0, data $list]); }这里的with是ThinkPHP的关联预加载避免循环查询数据库。前端拿到的数据里每一条已经包含老师姓名、课程名称、教室名称不需要再发额外请求。4. uniapp小程序端实现与课表渲染4.1 页面结构与数据流设计uniapp项目里课表页是核心。页面结构大概是schedule/ ├── index.vue # 周课表主页面 ├── detail.vue # 课程详情 └── adjust.vue # 调课申请页index.vue的数据流是进入页面 → 获取当前周 → 请求课表接口 → 收到数据后按“星期几 节次”映射到网格模型 → 渲染课表。请求接口时我建议在前端统一封装请求工具比如client/api/request.jsconst BASE_URL https://your-api-domain.com; export function request(url, method GET, data {}) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, token: uni.getStorageSync(token) }, success: (res) { if (res.data.code 0) { resolve(res.data.data); } else { uni.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }统一封装的好处是后续如果要在请求头加loading、加token刷新逻辑、统一错误处理只需要动一个文件。4.2 周课表网格渲染方案渲染课表最直接的方式是用CSS网格布局把“第几节”作为纵轴“星期几”作为横轴然后把课程记录填充到对应网格里。我用的渲染结构view classschedule-grid view classschedule-header view classtime-slot/view view classweek-day v-forday in 7 :keyday{{ weekNames[day - 1] }}/view /view view classschedule-body view classsection-row v-forsection in maxSection :keysection view classsection-label{{ section }}/view view classsection-cell v-forday in 7 :keyday !-- 这里根据 scheduleMap 填充课程 -- /view /view /view /view然后把接口返回的数据处理成一个二维映射const scheduleMap {}; scheduleList.forEach(item { const key ${item.week_day}_${item.start_section}_${item.end_section}; if (!scheduleMap[item.week_day]) { scheduleMap[item.week_day] {}; } for (let i item.start_section; i item.end_section; i) { scheduleMap[item.week_day][i] item; } });这样在模板里就只需要通过scheduleMap[day][section]去查有没有课有课则显示课程卡片没课则显示空白格子。这种方案的优点是直观、容易理解也不依赖第三方组件库。缺点是课程卡片是“按节次一格一格填充”的如果一门课跨了3节它就是3个格子样式上无法做一个跨行的完整卡片。解决这个问题通常有两种思路一种是把3节课拆开分别显示课程信息简单但不美观另一种是用绝对定位计算卡片高度和位置效果好一点但代码复杂度会上升。我自己的经验是毕设和中小型项目用第一种够用了如果特别在意展示效果可以换用绝对定位方案等有空我单独写一篇。4.3 周次切换与日期计算排课系统对“周次”非常敏感一个学期有第1周到第20周同一门课可能只排在第2周到第12周。小程序端可以根据当前日期计算当前是第几周然后请求该教学周的课表。计算开学周的工具函数export function getCurrentWeek(semesterStartDate) { const start new Date(semesterStartDate); const now new Date(); const diffDays Math.floor((now - start) / (1000 * 60 * 60 * 24)); const week Math.floor(diffDays / 7) 1; return week 0 ? week : 1; }注意这里的日期计算必须考虑时区。如果你用PHP生成“开学时间”后端默认时区是UTC存进数据库的时间少了8小时前端拿过来解析就会出现日期偏移。建议前后端统一使用Asia/Shanghai时区PHP里用date_default_timezone_set(Asia/Shanghai)数据库连接串也加上timezone参数。4.4 课程卡片交互与状态展示课程卡片不能只是静态文字至少需要支持这些交互点击卡片弹出课程详情包含教师、教室、周次范围。教师端课程卡片上显示“申请调课”按钮。教务端显示“调课审批”入口。状态区分已发布、待调整、已调课用不同颜色区分。我设计课卡样式时会用颜色标记课程状态绿色正常排课黄色调课审批中红色时间冲突仅教务可见灰色调课已拒绝或已撤销这些状态在接口返回的status字段里已经定义好了前端只做映射。4.5 调课申请页面实现调课是排课系统里的高频业务。前端流程是教师点击课程卡片 → 选择“申请调课” → 进入调课页 → 选择目标时间星期几、节次→ 填写原因 → 提交。提交时最核心的一点是要在提交前做一次“目标时间是否已有安排”的提示。虽然后端也会做冲突检测但前端提前拦截一次能让老师少提交无效申请体验会好很多。async function submitAdjust() { const payload { schedule_id: currentSchedule.id, new_week_day: selectedWeekDay, new_start_section: selectedStartSection, new_end_section: selectedEndSection, reason: reasonText }; try { await request(/api/schedule/adjust, POST, payload); uni.showToast({ title: 提交成功等待审批, icon: success }); } catch (e) { // 这里会拿到后端的冲突提示直接 toast 出来 } }调课申请提交后教务端在待办列表里看到申请点击同意或拒绝。同意的动作在后端本质上是“把原课表的status改为已调课并创建一条新课表记录”。5. 常见问题与排错技巧5.1 小程序端常用问题问题1请求接口报url not in domain list这是小程序开发最经典的问题。解决方法有两个在微信公众平台配置合法域名或在开发者工具里勾选“不校验合法域名”。我自己调试时建议直接勾选不校验域名方便本地开发。但上线前一定要把线上域名的HTTPS证书配好并且在小程序后台把“request合法域名”填进去。问题2真机预览时请求不到本地接口本地开发时localhost只能在本机访问。真机调试需要把接口地址改成电脑所在局域网IP比如http://192.168.1.100:8080并且手机和电脑要在同一个WiFi下。如果后端跑在虚拟机上还需要额外做端口映射。问题3下拉刷新后课表数据没更新这种情况99%是因为请求被缓存了。uniapp的uni.request默认没有关闭缓存你需要手动给请求加时间戳url: BASE_URL url ?_t Date.now()或者在小程序平台设置cache: no-cache。我个人习惯是统一在请求工具里加参数避免每个接口都担心缓存问题。5.2 PHP后端常见问题问题1数据库乱码统一使用utf8mb4字符集PHP文件保存时也用UTF-8无BOM格式。如果数据库已经建好了记得把表和字段的字符集都改过来。ALTER TABLE schedule_record CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;问题2Session不同步小程序不是浏览器环境它每次请求不一定会带Cookie依赖Session做登录态会不稳定。我的建议是使用token认证把token放在请求头里后端通过中间件解析。问题3接口返回JSON格式异常如果接口返回的结果里混入了BOM字符、警告信息或者PHP错误日志前端解析时会报错。排查思路是先用浏览器访问接口地址看响应内容是否纯净然后在入口文件开启调试模式及时看PHP错误日志。5.3 排课系统业务层面的坑坑1开学周和教学周没对齐有些学校第一周不是完整周或者有法定节假日调休直接按“第1周从周一算”可能不准。严谨的做法是在系统里维护一个“校历表”把每个教学周的起止日期配置进去课表查询按校历表计算。虽然增加了一点维护成本但比死算日期可靠得多。坑2没有处理单双周课程很多高校有“单周上课/双周上课”的课程安排比如隔一周上一次。如果数据结构里没有week_type字段0表示每周1单周2双周这类课程就无法表达。我建议在设计schedule_record表时就加上这个字段哪怕第一版用不到也能避免后续加字段的麻烦。坑3课程冲突检测遗漏“时间不重叠但教室换场时间不足”这是比较细节的场景一门课在第1节到第2节下一门课在第3节开始看起来不冲突但中间只有10分钟课间休息如果两个教室距离很远老师根本来不及跑过去。真正合理的检查应该加入“教室间距”或者“节次间隔”的约束。毕设项目可以不做但真实交付时建议加上这个检查逻辑。6. 项目部署与上线注意事项6.1 PHP后端部署要点用宝塔面板部署时记得做这几件事PHP版本建议用7.4以上ThinkPHP 6最低要求PHP 7.2。Nginx配置伪静态指向public/index.php。关闭ThinkPHP的调试模式.env里设置APP_DEBUG false。给runtime目录写权限。6.2 小程序发布流程uniapp项目在HBuilderX里发行小程序时需要先配置manifest.json里的小程序AppID。发行步骤如下在微信公众平台注册小程序账号拿到AppID。HBuilderX选择“发行 → 小程序-微信”。用微信开发者工具打开生成的dist/dev/mp-weixin目录。配置合法域名预览、上传代码。提交审核审核通过后发布。6.3 上线前必做的安全加固排课数据虽然不像金融数据那么敏感但涉及大量师生的课程信息和个人账号体系上线前不能完全不设防管理后台接口必须加权限校验不能只靠前端隐藏入口。所有参数必须做合法性校验防止SQL注入。用户密码如果涉及账号体系要用password_hash加密存储。后台接口建议加上操作日志方便出问题时回溯。7. 实操心得做排课系统这类项目我最大的体会有三点。第一数据库永远先于代码设计。你如果一开始就多花一天时间把排课表、调课表的时间字段设计好后面写接口和前端课表渲染会顺畅很多。反过来如果表设计草率做到一半发现没法表达“单双周课程”或者“跨节次课程”返工成本远超你的预期。别问我是怎么知道的。第二排课系统的核心不是CRUD而是约束检查。增删改查功能只要基础扎实谁都能写出来。但“教室不能冲突、老师不能冲突、班级不能冲突”这三条规则能不能又快又准地在写入前拦截住才是真正决定系统好不好用的分水岭。做的时候可以把冲突检测单独抽成一个服务类以后加规则比如“晚间课程不能排到第9节以后”只需要动一个地方。第三不要零散地写接口先把接口文档定义清楚。哪怕只有自己一个人开发也要在动手前把接口路径、请求参数、返回结构列出来。我自己用过Apifox直接在工具里定义接口、生成文档、跑自动化测试效率提升非常明显。前端联调阶段能少很多“你这返回字段是啥意思”“我这里报错你那边看得到吗”的扯皮。这个项目后续如果想扩展可以往这几个方向走基于WebSocket做调课审批的实时通知、导出Excel课表给教务存档、接入企业微信或公众号做消息推送。基础架构稳了这些功能都只是往上面叠积木而已。