ARTICLE DETAIL

资讯详情

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

微信小程序教务系统开发:权限模型与后端设计要点

微信小程序教务系统开发:权限模型与后端设计要点 简介这份基于微信小程序的教务管理系统资源面向高校计算机相关专业学生及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 解码出来的对象不要再从数据库现查 sessionrequireRole只解决“角色能不能进”行级权限仍然需要业务代码自己判断。参数说明角色字段用字符串常量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.jsnode 示例 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这样跨学期查历史数据才方便。表名字段类型说明studentidint PK学生主键studentstudent_novarchar(20)学号唯一索引studentnamevarchar(50)姓名studentclass_idint班级studentopenidvarchar(64)微信登录标识可空teacheridint PK教师主键teacherteacher_novarchar(20)工号唯一索引teachernamevarchar(50)姓名teacherdept_idint院系semesteridint PK学期主键semesternamevarchar(50)如“2024-2025-1”semesteris_currenttinyint1 为当前学期有个设计细节openid 不要只存在登录表里。学生可能换个微信号重新登录业务数据要始终认student_idopenid 只是登录凭证之一。登录时如果 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-7start_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、statusstatus 用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% 的交付翻车。教务系统数据敏感宁可多测一轮也别让老师在讲台上打不开课表。希望这些踩坑记录能帮你在启动时少趟几条浑水也祝你交付顺利。本文还有配套的精品资源点击获取
返回列表