简介:这份基于微信小程序的教务管理系统资源,面向高校计算机相关专业学生及Java课程设计开发者,解决移动端教务管理场景下的功能实现与前后端交互需求。资源包含课程报告Word文档和完整源码,源码以前端小程序为主,通过用户提交数据返回PC端教务管理系统,实现用户与教务管理系统的双向联动,适合用于课程设计、毕业设计参考或小程序开发入门学习。压缩包共130个文件,主要涵盖png界面图、js逻辑脚本、json配置、wxss样式、wxml页面结构以及docx报告等类型,目录按功能模块组织,便于对照文档梳理代码;整体大小约2.76MB,轻量易部署。已有984人学习下载。读者可获得完整项目源码、配套课程报告,以及从页面设计到数据交互的完整实现思路,尤其适合需要快速理解小程序端教务管理业务流程并在此基础上扩展功能的开发者。
1. 教务小程序不是“网页套壳”:先想清这三点再做
拿到“基于微信小程序的教务管理系统.zip”这类工程,很多团队的第一反应是把 PC 教务网站改成移动端样式。这个思路在门户站可行,在教务系统上大概率翻车。教务管理涉及学生、教师、管理员三类账号的权限隔离,又牵扯课表、成绩、选课、考勤这些强状态流转的数据,小程序端从启动、登录到数据落地,每一步和普通 H5 都不一样。这篇笔记按我从需求评审到真机调试的完整路线讲清楚:它到底是什么、最小可用版本怎么做、哪些坑值得提前避开。适合正在接毕设、外包或内部小工具的人,也适合第一次把微信小程序工程跑起来的新手。
2. 先定权限模型再动页面:三类账号的边界从接口开始
2.1 为什么不能只靠前端按钮藏角色
教务管理系统的第一原则:权限不是前端按钮显不显示的问题,而是后端接口、数据行级都得管的问题。很多毕设代码如下:管理员入口用wx:if="{{role === 'admin'}}"把按钮藏起来,学生拿不到教师管理入口,看似权限隔离,实际任何人只要打开开发者工具的 Network 面板,手动调一次/api/admin/courses就能拿到全部课程数据。前端隐藏按钮只影响交互入口,不影响数据安全。
我一般把角色拆成四类:学生(查课表、查成绩、选课退课、看通知)、教师(录入成绩、看任课课表、课堂考勤、发通知)、教务管理员(学期设置、课程编排、选课开关、账号管理)、系统管理员(管理教务管理员、看操作日志)。最小可用版本可以不引入辅导员这类角色,角色越少,权限矩阵越不容易互相打架。
| 功能域 | 学生 | 教师 | 教务管理员 | 系统管理员 |
|---|---|---|---|---|
| 查看课表 | 只看本人 | 只看任课班级 | 全部 | 全部 |
| 查看成绩 | 本人成绩 | 所授课程成绩 | 全部 | 全部 |
| 选课/退课 | 可操作 | 不可 | 维护选课开关 | 只读 |
| 成绩录入 | 不可 | 所授课程可录 | 可确认发布 | 只读 |
| 考勤管理 | 不可 | 所授课程可操作 | 全部 | 只读 |
| 系统设置 | 不可 | 不可 | 可操作 | 可操作 |
后端只按角色拦截还不够,教务系统的特殊性在行级权限。教师这个角色可以查成绩,但只能查自己教的班级,不能查全校成绩。所以我会在 token 里存role和userId,接口里再做一层行级过滤:查成绩强制WHERE teacher_id = req.user.id,而不是只判断“你是不是教师”。行级权限往往比角色权限更容易漏,预算评审时一定要写进去。
权限校验放在一个中间件里统一做,避免每个接口复制粘贴。示例代码(Node 风格):
const requireRole = (...roles) => { return (req, res, next) => { const user = req.user; // 由前面的 auth 中间件从 token 解出 if (!user || !roles.includes(user.role)) { return res.status(403).json({ code: 403, message: 'no permission' }); } next(); }; }; // 只有教务管理员可以维护学期 app.post('/api/semesters', requireRole('admin'), createSemester); // 教师只能操作自己所授课程的成绩 app.put('/api/scores/:id', requireRole('teacher'), async (req, res) => { const score = await db.findScoreById(req.params.id); if (score.teacher_id !== req.user.id) { return res.status(403).json({ code: 403, message: 'not your course' }); } // 继续业务逻辑 });这里的逻辑说明:req.user是登录接口签发的 token 解码出来的对象,不要再从数据库现查 session;requireRole只解决“角色能不能进”,行级权限仍然需要业务代码自己判断。参数说明:角色字段用字符串常量'student'、'teacher'、'admin',前端不要传角色,角色只能由后端登录接口写入 token。
2.2 页面骨架先定下来:tabBar、分包和页面栈
教务小程序的页面数量通常不少:首页、课表、成绩、选课中心、考勤、通知、个人中心,再加教师端成绩录入、管理员端学期设置。原生小程序的 tabBar 最多 5 个,而且不支持按角色动态显隐。我常用的做法是:三个 tab 固定为“首页、课表、我的”,学生进首页看到选课入口,教师进首页看到成绩录入入口,管理员进首页看到系统设置入口。角色差异在页面内部分流,而不是换一套 tabBar。
tabBar 配置在app.json里,注意pagePath一定在pages数组里声明过:
{ "pages": [ "pages/index/index", "pages/schedule/index", "pages/me/index" ], "tabBar": { "color": "#9ca3af", "selectedColor": "#1a73e8", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/schedule/index", "text": "课表" }, { "pagePath": "pages/me/index", "text": "我的" } ] }, "subpackages": [ { "root": "pkg-course", "pages": ["pages/list/index", "pages/detail/index"] }, { "root": "pkg-admin", "pages": ["pages/semester/index", "pages/account/index"] } ] }页面栈要提前规划好:课表详情、成绩详情这类从列表点进去的页面不要做成 tab 页,tab 页一旦多起来,wx.navigateBack层级容易乱。小程序的页面栈最多 10 层,深链超过 10 层会跳转失败,常见做法是详情页一律navigateTo压栈,返回靠navigateBack,避免用redirectTo破坏返回路径。分包设计上,tab 页必须放主包,非 tab 业务页全部拆到分包,这样主包体积能控制在 1MB 以内,后续发布和真机预览都更稳。
2.3 在微信开发者工具里跑通最小工程
新手拿到 zip 解压后,最容易卡在“项目如何启动”上。常见做法是打开微信开发者工具,选择“小程序”模式,点导入项目,目录选到包含app.json的那一层。很多人把目录选到了外层文件夹,开发者工具报“找不到 app.json”,其实是层级选错了。
最小启动步骤是这样的:
- 打开微信开发者工具,点“导入项目”,目录选择解压后的工程根目录(能看到
app.json的那一层)。 - AppID 先用测试号,或者注册一个小程序账号拿到正式 AppID;个人开发可以先不点“云开发”。
- 本地设置里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。
- 点“编译”,在模拟器里看页面是否正常渲染。
- 如果打开是白屏,先看 Console 报错;最常见的是
app.json里配置的页面路径不存在,或者 tabBar 图标路径写错。
这一步只完成了本地预览。要让别人在手机上试用,开发者工具里点“上传”,把代码提交到微信后台,再在后台“版本管理”里把上传版本设为体验版,添加体验成员后生成体验版二维码。这里最容易翻车的点是:上传前没有在“详情-本地设置”里确认 AppID 是正式 AppID,测试号上传会被后台拒绝。虽然叫“测试号”,但它只适合模拟器调试,不适合走体验版流程。
3. 登录链路与服务端接口:把“能打开”变成“能登录”
3.1 wx.login 到底在登什么
微信小程序登录不靠账号密码,核心是wx.login拿 code,后端用 code 换 openid。openid 是用户在微信体系里的唯一标识,同一用户在不同小程序里 openid 不同,但在同一个小程序里永远不变。教务系统里用它绑定学号,以后每次请求都通过 token 识别用户身份。
前端示意代码:
// pages/login/index.js wx.login({ success: async (res) => { const { code } = res; const loginRes = await request({ url: '/api/auth/login', method: 'POST', data: { code } }); wx.setStorageSync('token', loginRes.token); wx.setStorageSync('role', loginRes.role); wx.switchTab({ url: '/pages/index/index' }); } });后端拿到 code 后,调微信的jscode2session接口换 openid:
// server/routes/auth.js(node 示例) const axios = require('axios'); const jwt = require('jsonwebtoken'); app.post('/api/auth/login', async (req, res) => { const { code } = req.body; const { data } = await axios.get('https://api.weixin.qq.com/sns/jscode2session', { params: { appid: process.env.WX_APPID, secret: process.env.WX_SECRET, js_code: code, grant_type: 'authorization_code' } }); // data.openid 是用户唯一标识 const token = jwt.sign( { openid: data.openid, role: 'student', userId: null }, process.env.JWT_SECRET, { expiresIn: '7d' } ); res.json({ token }); });几个参数要敲黑板:code5 分钟有效,而且只能用一次,后端换取后立即作废;session_key只在服务端解密用户信息时使用,绝不能下发到前端;openid不能当作用户主键直接暴露。教务系统里学生第一次登录后要绑定学号,绑定完成后后端把userId和role一起写进 token,后续所有业务接口都从 token 拿身份。不要试图用微信昵称判断“是不是这个学生”,头像昵称在小程序里需要用户主动填写,而且美化过的昵称不具备身份唯一性。
3.2 请求封装:token 自动携带与 401 跳登录
原生的wx.request回调风格写业务很啰嗦,也不便于统一处理登录态失效。我会在项目里封装一个 Promise 风格的request,自动携带 token,统一处理 401。注意小程序没有 axios 的拦截器机制,这个封装是所有页面的公共入口,后续加日志、加错误提示都只改这一处。
// utils/request.js const request = (options) => { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Authorization': token ? `Bearer ${token}` : '', 'Content-Type': 'application/json', ...(options.header || {}) }, success: (res) => { if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.removeStorageSync('role'); wx.navigateTo({ url: '/pages/login/index' }); reject(new Error('unauthorized')); return; } if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { // 统一错误提示,业务代码不用重复写 toast wx.showToast({ title: res.data?.message || '请求失败', icon: 'none' }); reject(res); } }, fail: (err) => { // fail 里不要静默吞掉,线上排查全靠这一条 console.error('[request failed]', options.url, err); wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }; module.exports = request;这段封装有两点要说明:token 放在Authorization头里是常见做法,不要为了省事把 token 拼在 URL query 上,否则日志和抓包工具里一抓一个准;401 处理要放在 promise 状态流转之前,否则业务代码会先收到 reject,页面里还在用旧 token 继续发第二波请求。另一个容易被忽略的点:wx.navigateTo不能跳 tab 页,如果登录页恰好是 tab 页,就要换成wx.switchTab,我一般把登录页设计成非 tab 页面,省得踩这个分支。
3.3 云开发还是自建后端:一次选型避免后期迁移
教务系统后端有两种常见路线。微信小程序云开发免运维、免服务器,自带云数据库和云函数,登录鉴权有现成的cloud.getWXContext()拿 openid,适合毕设、原型和中小型内部工具。自建后端(Node/Java/PHP + MySQL)则灵活得多,适合要长期迭代、有多端需求、或已经有 PC 教务系统的团队。两者选型要一次性想清楚,别开发到一半再迁移。
| 对比维度 | 云开发 | 自建后端 |
|---|---|---|
| 服务器成本 | 按量计费,有免费额度 | 需要买服务器和带宽 |
| 登录鉴权 | 云函数直接拿 openid | 自建 code2Session + JWT |
| 数据库 | 文档数据库,弱事务 | MySQL/PostgreSQL,强事务 |
| 文件存储 | 自带云存储 | 需自己接 OSS/COS |
| 域名备案 | 不需要 | 必须备案 HTTPS 域名 |
| 适合场景 | 毕设、原型、内部工具 | 生产系统、多端复用 |
从教务系统的选课场景看,我倾向有 MySQL 经验的团队直接自建后端。选课发课瞬间是强事务场景,文档型数据库做“剩余名额扣减”和“防止重复选课”这类操作要自己实现事务,不如关系型数据库一行UPDATE course SET remaining = remaining - 1 WHERE id = ? AND remaining > 0来得直接。如果已经选了云开发,也不慌,把云函数当作后端服务用,数据库选 MySQL 的云数据库实例,一样能跑。
还要提醒一个上架成本问题:小程序上线要求接口域名是已备案的 HTTPS 域名,并且必须在小程序后台配置合法域名。教育类目对主体有要求,个人主体基本过不了教育类目审核;企业或事业单位主体需要认证,认证费用是每年 300 元,这个钱在预算阶段就要算进去,别等到提审时才被卡住。
4. 六张核心表设计与状态流:选课、成绩、考勤不乱
4.1 三张基础表:学生、教师、学期
教务系统的基础表不需要太多,先立住学生、教师、学期三张。学生的核心字段是学号、姓名、班级、openid;教师核心字段是工号、姓名、所属院系;学期表用来区分当前学期,业务表都挂semester_id,这样跨学期查历史数据才方便。
| 表名 | 字段 | 类型 | 说明 |
|---|---|---|---|
| student | id | int PK | 学生主键 |
| student | student_no | varchar(20) | 学号,唯一索引 |
| student | name | varchar(50) | 姓名 |
| student | class_id | int | 班级 |
| student | openid | varchar(64) | 微信登录标识,可空 |
| teacher | id | int PK | 教师主键 |
| teacher | teacher_no | varchar(20) | 工号,唯一索引 |
| teacher | name | varchar(50) | 姓名 |
| teacher | dept_id | int | 院系 |
| semester | id | int PK | 学期主键 |
| semester | name | varchar(50) | 如“2024-2025-1” |
| semester | is_current | tinyint | 1 为当前学期 |
有个设计细节:openid 不要只存在登录表里。学生可能换个微信号重新登录,业务数据要始终认student_id,openid 只是登录凭证之一。登录时如果 openid 没有匹配到学生,就跳“学号绑定”页面,绑定成功后建立 openid 与 student_id 的映射。教师同理,工号绑定后后续请求都以 token 里的userId为准。这样做的好处是,以后教务系统如果接入企业微信、独立 App,登录方式多端并存不冲突。
4.2 选课表和成绩表:并发、冲突与状态流
选课是教务系统里并发压力最大的接口。选课表course_selection至少有student_id、course_id、semester_id三个字段,而且必须加唯一索引uk(student_id, course_id)。没有这个索引,前端连点两次按钮就会插入两条重复记录,后面统计人数、排课都会乱。
选课时间冲突比重复选课更难处理。一门课有“星期几、第几节、单双周”这样的时间片字段,选课时要检查新课与已选课程是否时间重叠。我用一条带锁的查询来做:先锁学生已选课程的行,再查冲突,最后插入,放在事务里。下面这段是核心逻辑,语言换成 MySQL 也能直接看:
START TRANSACTION; -- 锁住该学生当前学期的选课记录,防止并发选课造成冲突漏判 SELECT course_id FROM course_selection WHERE student_id = ? AND semester_id = ? FOR UPDATE; -- 查新课程与已选课程的时间片是否有重叠 SELECT COUNT(*) FROM course_selection cs JOIN course c ON cs.course_id = c.id WHERE cs.student_id = ? AND c.weekday = ? AND c.start_section <= ? AND c.end_section >= ?; COMMIT;这里说明两个参数:weekday是星期几(1-7),start_section/end_section是节次区间,比如第 3-4 节就是 3 和 4。FOR UPDATE锁的是学生已选课的行,保证两个并发请求不会同时读到旧数据。课程容量控制也放在同一个事务里:UPDATE course SET remaining = remaining - 1 WHERE id = ? AND remaining > 0,受影响行数为 0 就说明没名额了,直接回滚。
成绩表设计上,不要只放一个score字段,要加状态字段status。我的状态流是:教师录入(draft)→ 教师提交(submitted)→ 教务管理员确认(confirmed)→ 发布(published)。学生端只查published状态,这比“录完就可见”安全得多——教师经常会先录一部分再调整,发布前学生看到半成品成绩,投诉电话就来了。成绩表字段:student_id、course_id、score、grade_point、status、entered_by,其中entered_by记录录入教师 id,行级权限判断要用它。
4.3 通知表、考勤表与缓存键设计
通知表要带target_role,因为教务系统里一条通知可能只发给教师,或只发给某一年级学生。target_role可以是all/student/teacher/admin,再配合target_class_id做班级定向。发布通知后不需要即时推送给所有人,小程序端下拉刷新时拉取新通知即可,保证数据最终一致就行。
考勤表按“课程 + 日期 + 学生”做唯一键。字段至少包含course_id、student_id、attendance_date、status,status 用present/late/absent/leave四个值。教师端录入考勤,通常是先选课程,再按日期拉学生名单,批量更新。这里要注意:考勤表如果不加UNIQUE(course_id, student_id, attendance_date),同一学生同一天会被录入两次,生成重复考勤记录。批量插入时用INSERT ... ON DUPLICATE KEY UPDATE status = VALUES(status)一次性合并,避免先查再改的两步逻辑。
缓存键设计容易被忽视。课表和成绩是高频查询、低频变更的数据,适合在小程序端做本地缓存。我会把 key 设计成带上下文的:schedule:{studentId}:{semesterId}、score:{studentId}:{semesterId},其中 semesterId 当前学期变了,key 自动失效,不需要手动清缓存。列表加载更多的场景注意:分页列表不要整体缓存,缓存第一页就够了,否则下拉刷新和点击“加载更多”容易互相打架。后端课程表变更后,最好主动清理对应学生的缓存,而不是等客户端自己拉,这个操作在管理端修改排课时顺手调一遍即可。
5. 教务小程序避坑排查:五条换真金白银的踩坑记录
5.1 选课瞬间后端被打挂:事务与唯一索引
现象:选课开放后 30 秒内,服务端 CPU 飙高,数据库里出现多条重复选课记录,部分学生报错“选课失败”但库里实际已经插入成功。
原因:选课接口没有事务保护,也没有唯一索引;前端按钮没有做防止连点处理,学生双击两次就发两个请求。更隐蔽的是,Python 或 Node 后端如果先查再插,两个请求同时读到“没有选过”,就都插入成功了。
解决:先给course_selection表加UNIQUE(student_id, course_id),数据库层面的重复检查兜底;再把“查冲突 - 扣容量 - 插入”包进事务。前端按钮点击后立刻置为disabled,并加一个短时间防抖。线上事故之后我再没让选课接口裸奔过,唯一索引是最便宜的后悔药。
5.2 订阅消息授权弹窗只出现一次
现象:第一次调用wx.requestSubscribeMessage,用户点了允许,之后同样的模板消息再也推不出,按钮也不弹窗了。
原因:微信小程序订阅消息的授权是一次性的。用户每次允许,开发者只能使用一次模板消息;下一次推送给该用户前,需要用户再次触发授权弹窗。很多开发者误以为授权一次就能长期推送,结果第二周所有推送都静默失败。
解决:把订阅消息用在低频、强触达的场景,比如“选课成功通知”“成绩发布通知”。每次用户要触发这类通知时,在对应操作按钮里重新调wx.requestSubscribeMessage,不要试图在启动时集中弹窗要授权。高频场景(比如每日课表提醒)不适合用订阅消息,微信对这类模板有严格的类目限制,建议直接用站内信 + 下拉刷新代替。
5.3 source size 2612kb 超过 2MB:拆分包的正确姿势
现象:开发者工具“上传”时报错,提示source size 2612kb exceed max limit 2mb,代码传不上去。
原因:这个报错指的是主包超限。微信小程序总包上限是 20MB,但单个分包(包括主包)不能超过 2MB。2612kb 通常意味着 tab 页、公共组件、静态图片全堆在主包里,页面多时主包体积很容易爆。
解决:把非 tab 业务页拆到分包。app.json里用subpackages声明,root是分包根目录,pages是该分包下的页面路径。课表详情、成绩详情、选课中心、管理端页面都可以进分包。公共代码和 tab 页留在主包,分包页面可以引用主包的组件和工具函数,反向不行。图片不要直接放项目assets里,大图全部走 CDN 或云存储,本地只留压缩过的占位图。拆完之后主包低于 1MB 才算健康。
5.4 开发者工具正常、真机数据不更新
现象:模拟器里改了数据一切正常,真机预览时课表和成绩还是旧值,有的用户清掉小程序进程再打开就正常了。
原因:八成是本地缓存没清,另外两个常见来源是wx.setStorageSync写入了过期 key,以及 GET 请求被默认缓存。开发者工具默认开了“不校验合法域名”,这个设置下接口请求相对干净,真机上如果服务器返回头带了缓存字段,小程序端wx.request也可能直接用本地缓存。
解决:开发阶段在工具里点“清缓存-清除数据缓存”。代码里给列表请求显式传cache: 'no-cache',服务端接口统一加Cache-Control: no-store。本地缓存统一走带学期号或版本号的 key,比如schedule:1001:20241,改学期自动失效,不用每次发版手动清。真机上有问题时,优先看小程序真机调试的 Network 面板,别用模拟器结果推断真机行为。
5.5 调试时看不到请求:抓包与代理的坑
现象:后端日志里没有请求记录,前端也没有报错提示,页面上表现为“点了没反应”。
原因:wx.request在fail回调里如果没有打日志,很多失败是静默的。域名证书校验失败、请求被代理拦截、开发阶段合法域名没配,都会让请求直接进fail,而页面代码往往只处理了success。
解决:请求封装的fail回调里一定加console.error和用户可见 toast,错误最怕看不见。开发阶段可以用抓包工具(Charles、Reqable 这类)配系统代理看完整请求链路,但要注意小程序会校验 TLS 证书,证书装错位置会导致所有请求报错,调试完记得移除代理。微信开发者工具的本地设置里“不校验合法域名”只对工具生效,真机预览仍然受合法域名限制,所以开发阶段就应把线上环境的 HTTPS 域名配好,否则真机联调永远比模拟器慢半拍。
6. 收尾:上线前半小时检查清单与一个提效习惯
6.1 上线前挨个过一遍的检查项
进入提审前,我会把下面这张表打开,逐项确认。表格比口头约定靠谱,每项都是真实项目里卡过壳的地方。
| 检查项 | 怎么做 | 常见翻车点 |
|---|---|---|
| 合法域名 | 微信后台配好 HTTPS 已备案域名 | 开发结束才备案,审核等一周 |
| 类目主体 | 教育类目需企业/事业单位主体 | 个人主体过不了教育类目 |
| 主包体积 | 确认主包低于 1MB | 图片和公共代码堆在主包 |
| 登录链路 | 真机跑 login + 学号绑定 + token 刷新 | 只测了模拟器 |
| 成绩状态 | 学生端确认只能看到 published 成绩 | 教师提交后学生提前可见 |
| 默认密码 | 管理员与教师首次登录强制改密 | 用 admin/123456 上线 |
| 隐私弹窗 | 配置用户隐私保护指引 | 提审被驳回要求补协议 |
6.2 留给下一个交付者的交接习惯
最后一个经验:代码里留一个README.md或config.example.js,写清楚 AppID、Secret 环境变量、数据库连接串存放位置,密钥永远不要进仓库。之前接过一个外包盘,上一任把 AppSecret 直接写死在 config.js 里还提交到了 Git 历史,接手后第一件事是重置密钥,费了不少功夫。
我现在养成的习惯是,每个教务小程序交付前都自己从头跑一遍:解压、导入、登录、选课、录成绩、发布成绩、真机预览。这半小时能过滤掉至少 80% 的交付翻车。教务系统数据敏感,宁可多测一轮,也别让老师在讲台上打不开课表。希望这些踩坑记录能帮你在启动时少趟几条浑水,也祝你交付顺利。
本文还有配套的精品资源,点击获取