微信小程序教务系统课设:云函数与联表查询实战指南 📅 发布时间:2026/9/1 8:01:29 👁 浏览次数: 简介本资源是一个面向高校师生的微信小程序教务系统课程设计项目聚焦移动端课程表与成绩查询核心功能解决传统教务系统访问不便、响应滞后等问题适用于计算机专业本科高年级或毕业设计阶段的全栈开发实践。压缩包共166个文件含35个JavaScript云函数逻辑文件实现联表查询与数据聚合、49个JSON配置与接口定义、23个WXSS样式文件及21个WXML页面结构文件辅以PNG/JPG素材与附赠说明文档整体体积仅1.72MB结构清晰、模块解耦。资源已获45人学习下载提供完整可运行的小程序源码、云开发环境配置说明、多表关联查询SQL逻辑注释及二维码部署指引特别包含课程-教师-学生三表联查的云函数实现范例与调试要点便于快速理解教育类业务中复杂关系的数据建模与云端处理流程。1. 这个课设到底在做什么先掰扯清楚核心需求每年到了课程设计季总有不少同学私信我同一个问题微信小程序做教务系统到底怎么做才像一个真正的系统很多人交上去的作业无非是小程序里套几个静态页面课程表是写死的JSON成绩查询点进去永远显示同一组分数——这其实不叫教务系统这叫HTML静态网页换了个壳。这个项目标题里真正值钱的东西其实是后半句云函数编写、联表查询。教务系统这种业务天然就是多数据源关联的业务——学生、课程、成绩、选课记录、教师信息都不是孤立存在的。你查一张成绩单背后至少关联了学生表、课程表、选课表。如果没用到联表查询你的系统就只是表面功夫答辩时老师追问两句就能拆穿。这个课设的目标用户很明确高校师生。教师看课表、录成绩、查学生名单学生查课表、查成绩、看学分。移动端的价值在于随时查——不用特意跑教务处的电脑端网站也不用下载App微信里点开就能用。从这个角度说小程序确实是比Web端和App端都更合适的载体这也是选题选得对的地方。我从这个项目里抽出来最核心的技术骨架是这样的前端微信小程序原生开发负责页面展示和用户交互后端微信云开发云函数省去自己买服务器、配域名、搞HTTPS的环节数据层云数据库使用联表查询满足跨集合的数据整合需求很多同学一听到云开发就觉得是偷懒其实不然。云开发只是把运维层面的活替你干了业务逻辑、数据建模、查询性能优化这些硬功夫一点都省不掉。恰恰相反正因为省掉了环境搭建的时间你才有精力把联表查询、权限控制这些真正有区分度的功能做扎实。2. 数据模型设计联表查询的地基没打牢后面全是坑聊联表查询之前必须先搞清楚教务系统的数据模型。我在帮人改这个课设的时候见过最多的错误就是成绩表里直接存课程名字段而不是存课程ID。这么做看起来查询方便——拿到成绩数据直接显示课程名多省事。但等到你要做某位老师这学期教的所有课程或者某门课所有学生的成绩分布时这套设计就废了数据冗余和更新不一致的问题会让你怀疑人生。2.1 核心集合的设计与字段规划一个合格的、能撑起联表查询的教务系统至少需要以下集合students集合学生表{ _id: student_001, studentNo: 20240001, name: 张同学, major: 计算机科学与技术, grade: 2024, openid: 用户唯一标识 }courses集合课程表{ _id: course_001, courseNo: CS101, courseName: 数据结构, teacherNo: T001, teacherName: 李老师, credit: 4, semester: 2024-2025-1 }course_schedule集合开课/排课表{ _id: schedule_001, courseId: course_001, teacherNo: T001, dayOfWeek: 3, // 周三 startSection: 1, // 第1节开始 endSection: 2, // 第2节结束 weekStart: 1, // 起始周 weekEnd: 16, // 结束周 classroom: 教学楼A301 }scores集合成绩表{ _id: score_001, studentNo: 20240001, courseId: course_001, score: 92, gpa: 4.0 }users集合用户绑定关系表{ _id: user_001, openid: oXkY..., studentNo: 20240001, role: student // 或 teacher }这个模型的关键在于成绩表通过studentNo和courseId关联学生和课程课程安排通过courseId和teacherNo关联课程和教师。所有跨表的查询都基于这些ID字段做连接——这就是联表查询的数据基础。用ID做关联而不是用名称字符串做关联是为了保证数据一致性和查询效率。假设某位老师改了个名字你只需要更新courses集合里的一条记录所有查询都自动生效如果当初存的是字符串你得遍历更新所有引用过这个名字的记录。2.2 为什么这么设计范式与反范式之间的取舍有同学会问教师姓名也冗余存在courses表里了这不是违反范式吗这里我要解释一下云开发数据库的实践原则。在传统关系型数据库里我们追求高范式尽量减少冗余。但在云开发的文档型数据库中适度冗余是常态。因为有的时候你要查我本学期所有课程的任课老师叫什么如果courses表只有teacherNo你还得再去teachers表查一次姓名。与其一次查询带两次网络往返不如在写数据时就把teacherName冗余一份。关键是要掌握度——冗余的是不常变化的描述性信息比如姓名、课程名不能冗余的是会变化且需要统计的信息比如成绩、学分。成绩的学分怎么查成绩表里没有credit字段需要通过courseId去courses表拿。这就是联表查询要解决的核心问题之一。如果直接把credit冗余到成绩表里看起来方便了但如果培养方案调整学分历史成绩的学分就全错了。所以学分必须通过关联查询实时获取——这就是该冗余的冗余不该冗余的坚决不冗余的实践注解。2.3 权限边界与身份绑定逻辑小程序端用户登录后云函数能拿到用户的openid。这个openid是每个微信用户在小程序下的唯一标识。方案设计上用户第一次进入小程序时需要输入学号和姓名云函数用openid 学号做绑定写入users集合。此后每次请求云函数就根据调用者openid去users集合查角色再根据角色决定返回哪些数据。学生查成绩只能查自己的老师查成绩可以查所授课程所有学生的。这个逻辑必须在云函数端实现绝不能放在前端通过if语句控制——因为前端代码是可以在开发者工具里改的攻击者只需要改几个变量就能越权访问。云函数端的权限校验是安全底线。3. 云函数的设计与编写核心业务逻辑的真正归属这部分是整个项目的灵魂也是答辩时最容易被深挖的部分。我见过太多课设把业务逻辑写在小程序端云函数纯粹当摆设——那样做云函数存在的意义就没了。3.1 云函数在项目里到底扮演什么角色微信云开发架构下云函数相当于你自己服务器上的后端接口但它跑在云端托管环境里用Node.js运行。小程序端通过wx.cloud.callFunction调用云函数云函数操作云数据库把结果返回给前端。这个链路里云函数是唯一能操作数据库的可信入口。为什么非要经过云函数直接在小程序端用wx.cloud.database()操作数据库不行吗技术上行但有两个致命问题第一权限控制颗粒度不够。云数据库的权限设置虽然是可配置的但只能做所有人可读仅创建者可读写这种粗粒度控制你做不了学生只能查自己的成绩这种行级控制。行级控制需要你在查询条件里带openid: 当前用户openid——但前端传参数是可以伪造的。第二复杂查询做不了。联表查询聚合操作在小程序端支持有限而且多表数据组装放前端会导致请求次数爆炸。云函数是Node.js环境聚合管道的能力完整网络延迟也低得多。所以联赛查询这种活必须交给云函数。3.2 云函数的目录结构与路由约定我倾向于做一个统一入口式的云函数而不是每个业务建一个云函数。原因很简单云函数数量多了部署、调试都是麻烦事而且每个云函数都要打包冷启动时间体验反而差。我的做法是建一个名为api的云函数内部用action参数做路由分发cloudfunctions/ └── api/ ├── index.js // 入口按action分发 ├── package.json └── actions/ ├── getSchedule.js // 课表查询 ├── getScores.js // 成绩查询 ├── getCourses.js // 课程列表 ├── bindUser.js // 用户绑定 └── getTeacherCourses.js // 教师端课程列表index.js核心分发的逻辑长这样const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event, context) { const { action, payload } event const openid cloud.getWXContext().OPENID switch (action) { case getSchedule: return await require(./actions/getSchedule).main(openid, payload, cloud) case getScores: return await require(./actions/getScores).main(openid, payload, cloud) case bindUser: return await require(./actions/bindUser).main(openid, payload, cloud) default: return { code: -1, msg: 未知action } } }注意这里有个容易踩坑的细节cloud.init必须带env: cloud.DYNAMIC_CURRENT_ENV否则在本地调试时连不上云端数据库。这个参数的意思是使用当前云环境不加的话默认用第一个创建的环境如果环境配置不一样会报环境不存在的错。3.3 一个完整的云函数实例成绩查询以学生查成绩为例这是最常见的功能。业务逻辑梳理出来其实不复杂校验当前用户身份取出studentNo按studentNo查scores集合得到该学生的所有成绩用成绩里的courseId数组去courses表批量查课程信息把课程信息和成绩拼装后返回但如果你在scores集合里一个文档一个文档地查假设一个学生4年选了40门课你需要40次查询每次查询还有网络往返的开销。这个方法性能太差了。正确姿势是用_.in做批量查询一次拿出所有成绩记录然后再对courses表做一次_.in查询两次查询搞定全部数据。看代码// actions/getScores.js const _ require(lodash) async function getScores(openid, payload, cloud) { const db cloud.database() const $ db.command // 1. 校验身份 const userRes await db.collection(users).where({ openid }).get() if (userRes.data.length 0) { return { code: -1, msg: 用户未绑定请先绑定学号 } } const user userRes.data[0] if (user.role ! student) { return { code: -1, msg: 权限不足 } } // 2. 查学生所有成绩 const scoreRes await db.collection(scores) .where({ studentNo: user.studentNo }) .get() const scores scoreRes.data // 3. 批量查课程信息 const courseIds scores.map(s s.courseId) const courseRes await db.collection(courses) .where({ _id: $.in(courseIds) }) .get() const courseMap {} courseRes.data.forEach(c { courseMap[c._id] c }) // 4. 组装返回 const result scores.map(s ({ courseName: courseMap[s.courseId] ? courseMap[s.courseId].courseName : 未知课程, courseNo: courseMap[s.courseId] ? courseMap[s.courseId].courseNo : , credit: courseMap[s.courseId] ? courseMap[s.courseId].credit : 0, score: s.score, gpa: s.gpa, semester: courseMap[s.courseId] ? courseMap[s.courseId].semester : })) return { code: 0, data: result } }这里的$.in是云开发数据库命令提供的批量匹配操作相当于SQL里的WHERE courseId IN (...)。一次查询最多能匹配多少个ID云端默认单次查询最多返回100条记录所以要保证课程数不超过这个上限实际场景完全够用。注意这段代码里我一直没用真正的联表查询。这就是接下来要重点说的问题。4. 联表查询的实战集合聚合与分步组装怎么选这个项目标题里专门点了联表查询说明这是一个必须体现的技术点。但很多同学做联表查询时容易僵硬——听到联表就以为一定要用scores.aggregate().lookup()这种聚合管道语法。其实云开发数据库的聚合操作在语法和使用场景上和传统SQL的JOIN还是有差异的。4.1 云开发聚合lookup的真面目云开发的聚合聚合支持lookup操作语法大致如下const res await db.collection(scores).aggregate() .lookup({ from: courses, localField: courseId, foreignField: _id, as: courseInfo }) .match({ studentNo: 20240001 }) .end()这段代码做的事拿scores集合的courseId字段和courses集合的_id字段做匹配匹配上的数组挂在courseInfo字段下然后筛选出studentNo对应的记录。但lookup有个大坑它做的是一对多关联。如果scores集合里有多条记录对应同一门课courseInfo就会是一个数组。虽然我们在这个场景里scores表的courseId是同一条选课记录不会出现这个问题但如果你对一对多关系缺少预判后面组数据时很容易出现取.courseInfo[0].courseName还是.courseInfo.courseName这种低级错误。还有一个隐藏性能问题lookup在云开发里的执行方式是逐个文档去关联目标集合。如果scores集合有1000条记录每条的courseId都要去courses集合里查一次这1000次子查询的总耗时可能远超你的预期。数据量小的时候看不出来但到了真实教务系统那种规模性能就会出问题。所以在云函数里我实际更推荐分步查询、内存组装的做法查主表scores拿目标记录提取关联字段courseId去重用$.in批量查副表courses在内存里组装成Map再合并到结果集这种做法的可读性强每个步骤都能单独console.log调试出错了也好定位。而lookup一旦结果不对你很难判断是哪一步匹配出了问题。4.2 什么时候该真正用lookup如果项目里存在需要跨集合统计的场景lookup反而是更优解。举个例子老师想查我教的这门课所有学生的成绩分布——按分数段分组统计。const res await db.collection(scores).aggregate() .lookup({ from: students, localField: studentNo, foreignField: studentNo, as: studentInfo }) .match({ courseId: course_001 }) .group({ _id: { range: $.switch({ branches: [ { case: $.gte([$score, 90]), then: 90-100 }, { case: $.gte([$score, 80]), then: 80-89 } ], default: 60以下 }) }, count: $.sum(1) }) .end()这个场景如果用分步组装你得先把所有成绩记录拉回来再在内存里写一堆if-else做分组——代码又臭又长性能还差。聚合管道把统计逻辑放在数据库端执行返回给云函数的只是统计结果传输数据量小得多。所以判断标准很简单如果查询数据量小百级以内分步组装简单、可控、调试方便如果涉及统计、分组、排序后再取部分数据用lookup配合group、sort更高效。这个项目里我建议两种都写出来。答辩时问起来你既能说清楚分步组装的可靠性也能展示lookup在统计场景的威力技术层次感一下就出来了。4.3 联表查询里的字段引用与别名陷阱玩聚合操作时有一个语法细节特别容易踩坑$字段引用。在lookup后的group里你要引用的是lookup产生的数组字段里的子字段得写成$studentInfo.studentName引用原始字段写$score。一旦写错返回的不会报错而是字段名直接不存在查出来全是null——这种错误排查极其耗时。另一个陷阱是外键字段类型不一致。courses集合的_id是字符串但如果你在scores集合里存的courseId是数字比如某些导入工具自动转换的lookup和$.in都会匹配不上。这个坑在联表查询里排第一。我写了个辅助函数专门做类型校验function normalizeIds(ids) { return ids.map(id String(id)) }不管数据来源是Excel导入还是手动录入统一在云函数入口处做一次String转换能避免大量查不到数据的灵异问题。这也是我在调试项目时排除了半天的教训。5. 课表查询功能的特殊处理星期、节次与周次的组合逻辑课表查询在教务系统里属于看起来简单做起来琐碎的功能。前端展示上就是一个7列或5列的网格但后端的组合逻辑和边界条件比成绩查询复杂得多。5.1 周次判断单双周与第几周的坑高校课表有单周、双周、连续多周三种情况。比如1-16周单周上课说明双周没课。course_schedule集合里我建议用weekList字段直接存一个有课周的数组比如[1,3,5,7,9,11,13,15]。这样查询时逻辑最简单const currentWeek payload.week // 从哪来见下文 const scheduleRes await db.collection(course_schedule).get() const validSchedules scheduleRes.data.filter(s s.weekList.includes(currentWeek) )如果你用经典的weekStartweekEndweekTypeall/odd/even设计判断逻辑也不复杂function isWeekValid(schedule, currentWeek) { if (currentWeek schedule.weekStart || currentWeek schedule.weekEnd) { return false } if (schedule.weekType all) return true if (schedule.weekType odd) return currentWeek % 2 1 if (schedule.weekType even) return currentWeek % 2 0 return false }5.2 当前周次从哪来这里有个关键决策课表要按当前是第几周来展示那这个周次数据怎么拿到方案一小程序端用户手动选择周次。实现简单但体验差谁也不想每次打开手动数现在是第几周。方案二写一个云函数返回校历数据配置好学期开始日期在云函数端计算当前周次。这个方案更合理。云函数端用服务器时间不是用户本地时间避免用户改手机时间作弊function getCurrentWeek(semesterStartDate, semesterEndDate) { const now new Date() const start new Date(semesterStartDate) const diffDays Math.floor((now - start) / (1000 * 60 * 60 * 24)) const week Math.floor(diffDays / 7) 1 // 边界控制 if (week 1) return 1 if (week 20) return 20 return week }校历数据存到academic_calendar集合里一个学期一条记录。云函数读出来缓存起来不必每次查数据库。在实际写这个方案的时候我把日期边界处理也放在同一函数里——开学前统一返回第1周学期结束后返回已结课这样前端处理逻辑会简单很多。5.3 课表数据的组装性能优化课表查询要展示的数据需要把course_schedule、courses、teachers三个集合的数据组装到一起。如果循环查数据库一个小程序页面加载要发几十次请求那是灾难。优化思路课程安排本来就是一个学期相对稳定的数据完全可以在用户第一次打开课表页时一次性把本学期所有课程数据查出来。一个学生一学期最多十几门课数据量极小做全量查询完全没问题。这才是课表查询的正确姿势// 一次查所有排课 const scheduleRes await db.collection(course_schedule) .where({ semester: semester }) .get() // 提取课程ID列表去重 const courseIds [...new Set(scheduleRes.data.map(s s.courseId))] const courseRes await db.collection(courses) .where({ _id: _.in(courseIds) }) .get() // 提取教师ID列表去重 const teacherNos [...new Set(scheduleRes.data.map(s s.teacherNo))] const teacherRes await db.collection(teachers) .where({ teacherNo: _.in(teacherNos) }) .get() // 内存组装 const scheduleWithInfo scheduleRes.data.map(s { const course courseMap[s.courseId] || {} const teacher teacherMap[s.teacherNo] || {} return { ...s, courseName: course.courseName, credit: course.credit, teacherName: teacher.name, className: course.className || } })5.4 前端课表网格渲染的坐标计算后端数据组装好之后前端要用二维数组承载课表// 假设一周7天每天12节 const weekDays [周一,周二,周三,周四,周五,周六,周日] const sections [1,2,3,4,5,6,7,8,9,10,11,12] // 构建格子数据 const gridData [] sections.forEach(section { const row { section, cells: [] } weekDays.forEach((day, index) { const cell scheduleList.find( item item.dayOfWeek index 1 item.startSection section item.endSection section ) row.cells.push(cell || null) }) gridData.push(row) })这个find逻辑的问题在于如果一门课横跨两节第二节还会再匹配一次导致同一门课在网格里出现两次。解决方案是只在startSection section这一行渲染课程信息其余行留空或合并单元格。小程序里没有真正的单元格合并我是用rowspan的CSS样式通过动态类名控制的.course-cell { position: absolute; left: 14.28%; width: 14.28%; height: 100%; }用绝对定位模拟跨行比表格布局更灵活。实际调样式的时候每节课的高度要按节次数量算好——比如1-2节连上这门课的高度就是2节课的高度。这些纯前端细节在答辩演示时反而是加分项因为看着像个真的教务系统。6. 数据初始化与测试数据系统能不能跑起来的关键这是我最想强调的一个环节。很多同学代码写得没问题但演示时页面空白最后发现是数据库里一条测试数据都没有。教务系统这种课设没有数据所有查询功能都是空转。6.1 测试数据的构造思路在云开发控制台手工一条条插入数据太痛苦效率极低。正确做法是写一个专门的数据初始化云函数一次性把基础数据塞进数据库。// cloudfunctions/initData/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async () { const db cloud.database() // 构造课程数据 const courses [ { courseNo: CS101, courseName: 数据结构, teacherNo: T001, teacherName: 李老师, credit: 4, semester: 2024-2025-1 }, { courseNo: CS102, courseName: 操作系统, teacherNo: T002, teacherName: 王老师, credit: 3, semester: 2024-2025-1 }, // 更多... ] // 批量插入 for (const course of courses) { await db.collection(courses).add({ data: course }) } // 构造排课数据 const schedules [ { courseId: xxx, teacherNo: T001, dayOfWeek: 1, startSection: 1, endSection: 2, weekList: [1,2,3,...,16], classroom: A301 }, // 更多... ] return { code: 0, msg: 初始化完成 } }注意最后的这个return。云函数返回的内容会出现在云开发控制台的日志里方便你确认执行结果。初始化函数加个消息确认很直观不用专门去数据库里数记录条数。6.2 批量插入的Limit坑云开发数据库的add单次只能插入一条记录循环插入没问题但如果课程有几百条for循环几百次会非常慢。云开发数据库支持insert批量插入吗不支持。有没有变通方案有用Promise.all并发插入await Promise.all(courses.map(course db.collection(courses).add({ data: course }) ))但这里有个隐患并发数过多会触发云函数的内存超限或数据库连接数超限。我的经验是分批每批20条分批并发async function batchInsert(collectionName, dataArray, batchSize 20) { const db cloud.database() for (let i 0; i dataArray.length; i batchSize) { const batch dataArray.slice(i, i batchSize) await Promise.all(batch.map(item db.collection(collectionName).add({ data: item }) )) } }还有一个必须先处理的细节courses集合和course_schedule集合有外键关联。插入courses后要拿到返回的_id再作为courseId插入schedule。所以批量插入courses时要保留ID映射关系const courseIdMap {} await Promise.all(courses.map(async course { const res await db.collection(courses).add({ data: course }) courseIdMap[course.courseNo] res._id })) // 构造排课时用 courseIdMap[CS101] 拿到真实ID6.3 演示数据的真实感设计测试数据要有真实感否则答辩时老师一看就觉得假。我建了20个学生、10门课、每人每学期6-8门课的成绩数据。成绩分布也要合理——有90的有60多的有挂科的这样查询成绩单时展示效果才丰富。还有一点要给每个学生设置不同的课表不要全班共用一张课表。同一门课可以安排在不同时间给不同班级上课这样课表查询结果才有差异性。我在做测试数据时故意让两个学生的课表差了两节课演示时会自然露出数据差异显得系统是真的联起来的。6.4 调试联表查询时的关键手段调试云函数时控制台直接执行是最快的方式。云开发控制台提供了云函数的测试功能可以模拟调用在云开发控制台-云函数-选中api函数-云端测试填入测试用event比如{action:getScores,payload:{}}点击执行看返回结果和日志注意这个测试调用不会带真实用户的openid。如果需要模拟用户身份可以在cloud.getWXContext().OPENID拿不到的情况下增加一个兜底逻辑比如开发环境下从event里读取openidlet { OPENID } cloud.getWXContext() if (!OPENID event.devOpenid) { OPENID event.devOpenid // 本地/控制台测试用 }这个写法只建议在云函数测试阶段用上线前务必移除。否则任何知道devOpenid参数的人都能模拟任意身份调用接口安全风险不可控。7. 把系统跑通之后的那些坑我在实操中反复踩过的雷做一个完整课设写完核心代码只是第一步。在这个项目的调试和演示过程中我遇到了好几个不说根本想不到的问题整理出来分享给大家这些经验比代码本身更值钱。7.1 云函数冷启动超时第一次调用云函数冷启动时间可能达到3-5秒。如果前端没有设置loading态用户看到的就是点击查询没反应体验很差。解决思路云函数在第一次调用时会建立数据库连接池后续调用会快很多。前端要做好loading提示也可以在小程序启动后悄悄预热一次云函数// app.js onLaunch wx.cloud.callFunction({ name: api, data: { action: ping } })在api云函数里加一个ping分支返回ok。预热后用户真正查询时冷启动耗时已经过去大半。7.2 数据库权限配置这是一个一票否决的坑。云开发数据库默认权限是仅创建者可读写你的小程序如果直接读取集合会报权限错误。但即便你按我前面的架构所有读写走云函数云函数端的数据库操作不受权限控制限制——云函数有管理员权限。调试时有一个坑中坑你在云开发控制台手动插入的数据如果创建者是控制台管理员那么小程序端直接读会读到但如果数据是通过另一个环境导入的可能就查不到。遇到代码没问题但查不到数据时先去控制台手动查一下集合数据是否存在确认数据入库了再排查权限。7.3 数据排序的稳定性成绩查询和课表查询都需要排序。云开发数据库的orderBy方法只支持单字段排序db.collection(scores) .where({ studentNo: 20240001 }) .orderBy(semester, asc) .orderBy(courseNo, asc) .get()实测发现云开发对多字段orderBy支持并不完善在数据量较大的时候排序结果可能不稳定。我自己更稳的做法是查出数据后在云函数内存里用JavaScript sort自己排排序逻辑完全可控scores.sort((a, b) { if (a.semester ! b.semester) return a.semester.localeCompare(b.semester) return a.courseNo.localeCompare(b.courseNo) })7.4 课程表的周次筛选与显示冲突我在前端实现时遇到一个体验问题如果按当前周过滤课表那过了第16周整张课表就是空的看起来很尴尬。于是我在前端做了个兜底当前周没有课时自动展示第一周的课表并提示当前已结课展示第1周课表。这个小细节在答辩时体现出了你考虑了边界条件老师们比较吃这一套。7.5 教师端和学生的权限分流教务系统不止学生用教师也要用。教师端的功能包括查看所教课程、查看选课学生名单、录入成绩。云函数里的权限判断逻辑// actions/getTeacherCourses.js async function main(openid, payload, cloud) { const db cloud.database() const userRes await db.collection(users).where({ openid }).get() const user userRes.data[0] if (!user || user.role ! teacher) { return { code: -1, msg: 仅教师可访问 } } const courses await db.collection(courses) .where({ teacherNo: user.teacherNo }) .get() return { code: 0, data: courses.data } }这种简单清晰的权限判定逻辑要在所有云函数action里都加上别想着偷懒。老师如果问你怎么保证安全你就说每个云函数入口都做了身份校验安全边界在服务端不在前端——这个回答基本稳过。写在最后的实操建议如果你正在做这个课设或者准备答辩我掏心窝子给你几个具体建议第一把云函数日志用起来。云开发控制台能看到每个云函数的调用记录和console.log输出调试联表查询时多打印中间变量。我调试分步组装时会在每个步骤后打console.log(courseIds:, courseIds)日志一比就能看出问题在哪一步。第二提前准备好演示数据。答辩前把云函数都调用一遍截图留底免得现场网络出问题。你可以在草稿箱里准备一份带正常数据的录屏视频万一当场云函数超时至少能放视频证明功能是通的。虽然有点土但实用。第三多表ID的命名规范要统一。studentNo用字符串就别用数字courseId用_id就别自定义。我在项目里统一用String类型的业务编号studentNo、courseNo、teacherNo做主键关联虽然_id是小程序自动生成的但我仍然在每张表里保留业务编号字段做主关联。这样写代码时脑子不会乱。第四不要只抄代码把数据库设计文档写出来。我给自己项目画过一张简易的E-R图字段关系画清楚后云函数写起来几乎是机械活。数据结构想不清楚代码永远都是缝缝补补。做这个项目最深的体会是教务系统这种课设题技术栈并不高深难点全在数据关系梳理和边界条件处理上。你先把成绩查询要关联几张表想清楚把课表跨周跨节怎么展示想清楚后面的代码其实是水到渠成的事。希望这篇拆解能帮你少走点弯路把时间花在真正有价值的地方——比如把联表查询的代码写得漂亮一点把前端体验打磨得像个真产品。本文还有配套的精品资源点击获取