ARTICLE DETAIL

资讯详情

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

微信小程序高校选课系统毕业设计:从数据库设计到并发控制全解析

微信小程序高校选课系统毕业设计:从数据库设计到并发控制全解析 去年带一个学弟做完“基于微信小程序的高校学生选课系统毕业设计”前前后后折腾了三个月。这个选题在毕业设计里属于“经典中的经典”——看起来满大街都是但真正能把选课并发、时间冲突、微信登录态、容量控制这些点讲清楚、做利索的答辩时其实少之又少。正好最近又有几个同学在问我把当时的技术方案、设计思路、踩坑记录整理成文从需求拆分到数据库设计从前端页面到后端接口再到真机调试的常见坑一次说透。适合正在做这个课题的本科生、想用小程序做管理系统的同学以及打算从零手写一个完整全栈项目的开发者参考。1. 项目方案选型与整体设计思路1.1 为什么选微信小程序而不是Web网页或原生App高校选课系统面向的是学生和教务管理人员使用场景高度集中在校园内、课间、宿舍对“随时打开看一眼课表/抢课”的需求远大于对复杂数据处理的需求。微信小程序最大的优势是免安装、触达率高、分享方便学生扫个码就能用。相比Web网页小程序在移动端的适配成本低得多不用考虑浏览器兼容相比原生App又省掉了安装包、签名、上架审核的整套流程对毕业设计这种短周期项目非常友好。官方开发工具自带调试器、真机预览、云开发环境搭建半小时内搞定。另外小程序有统一的登录体系微信授权省去了自己设计账号密码注册的麻烦。当然也有代价比如必须走HTTPS合法域名、包体不能太大、部分API有审核门槛这些在设计之初就要留出余地。1.2 后端与数据库选型对比后端我推荐用 Spring Boot MyBatis-Plus MySQL理由很直接选课系统核心是“数据关系”和“事务并发”Spring Boot生态成熟、资料多、答辩时老师也认MyBatis-Plus提供单表CRUD和条件构造器写业务代码效率极高MySQL则完全够用没必要上PostgreSQL或Oracle。如果你不熟悉Java用Node.jsExpress/Koa MongoDB也能实现但在毕业设计里“技术主流度和可解释性”往往比“技术新潮”更重要。下表是我当时对比的几个组合组合方案优点缺点适合人群Spring Boot MyBatis-Plus MySQL资料最多事务控制成熟答辩友好度高环境配置相对重有Java基础想求稳Node.js Express MySQL轻量前后端都用JS事务和并发控制要自己多留意前端转全栈Django SQLite/MySQL自带admin后台快速Python环境部署稍麻烦Python熟练小程序云开发无需自己搭服务器免域名学生旧版选课需求复杂时不好写事务纯前端、想最短路径完成我最终选了第一种。选课这个场景对事务和并发的要求比较典型如果拿云开发写数据库触发器那套反而更绕。1.3 系统架构设计的四个层次整个系统我拆成了四层小程序前端、后端接口层、业务逻辑层、数据存储层。前端只负责页面展示和用户交互所有业务判断都放在后端。小程序前端页面抽象为“首页/选课大厅/我的课表/成绩/个人中心”五个Tab页加上登录页。后端接口层提供RESTful接口统一返回格式为{ code, message, data }。业务逻辑层处理选课/退课的事务、课表冲突判断、容量判断、权限校验。数据存储层MySQL使用InnoDB引擎。这样分层的核心理由是“小程序端不进数据库、不做核心判断”。如果把容量判断写在前端只要有人请求先发后改或者伪造请求绕过页面系统分分钟被选爆。毕业设计的功能未必多复杂但架构上的正确性要体现出来这也是答辩时最容易加分的地方。另外一个容易忽略的点是统一返回结构。我见过不少同学直接返回一个JSON对象前端拿到再猜字段。我当时的做法是{ code: 0, message: success, data: { ... } }code非0就代表失败。前端封装一个request函数统一处理遇到code不为0直接弹toast省去大量重复逻辑。这个统一契约一定要先定好否则前后端联调会非常痛苦。2. 核心功能模块与数据库设计细节2.1 功能模块拆解学生端和教务端选课系统按用户角色分学生端、教师端、管理员端。毕业设计通常重点做学生端教师和管理员可以简化。我当时把学生端作为核心教师端只做了“查看我教的课和选课名单”管理员端只做了“开设课程/教学班、维护学生名单”。学生端功能清单微信登录绑定学号浏览可选课程按学院、分类筛选选课有容量限制有时间冲突检测退课在退课截止时间内我的课表按周显示成绩查询按学期显示个人信息维护这里有一个很重要的认知选课系统不等于简单的“课程CRUD”。难点在于一门课程往往有多个教学班不同老师、不同时间、不同地点学生选的是教学班不是课程本身。所以数据库设计时不能只建一张“课程表”必须拆出“课程表”和“教学班表”。2.2 数据表设计七张核心表的字段与关系我的数据库最终设计了7张表学生表、教师表、课程表、教学班表、选课记录表、成绩表、学期表。逐一说字段。学生表student字段类型说明idbigint主键student_novarchar(20)学号唯一namevarchar(50)姓名majorvarchar(50)专业gradevarchar(10)年级openidvarchar(64)微信openidphonevarchar(20)联系方式openid字段一开始就留好否则后面接微信登录会发现无从下手。openid也可以建唯一索引防止一个微信反复绑定多个学号。课程表course字段类型说明idbigint主键course_codevarchar(20)课程编号course_namevarchar(100)课程名称creditdecimal(3,1)学分course_typevarchar(20)必修/选修departmentvarchar(50)开课学院教学班表course_section字段类型说明idbigint主键course_idbigint课程id外键teacher_idbigint教师idsection_namevarchar(50)教学班名称如“高等数学A-01班”capacityint容量selected_countint已选人数semestervarchar(20)学期如“2024-2025-1”week_startint开始周week_endint结束周day_of_weektinyint星期几1-7start_sectiontinyint开始节次end_sectiontinyint结束节次classroomvarchar(50)教室选课时间冲突判断全靠week_start、week_end、day_of_week、start_section、end_section这几个字段。不要为每一天、每一节重复建记录那样数据量巨大且无法优雅判断交叉。用“周几节次区间周次区间”描述一个教学班的排课信息。一个教学班可能有多个时间段吗比如周一第1节到第2节在A楼周三第3节到第4节在B楼。如果这样就应该拆成多条“上课时间记录”不要为了省事放一个字段里。我一开始简单设计了单个时间段后来发现很多课一周两次只能重构。所以教学班表应该拆成“教学班主表 教学班时间表”时间表记录section_id, week_start, week_end, day_of_week, start_section, end_section, classroom。下面是教学班时间表教学班时间表section_time字段类型说明idbigint主键section_idbigint教学班idweek_startint开始周week_endint结束周day_of_weektinyint周几start_sectiontinyint开始节次end_sectiontinyint结束节次classroomvarchar(50)教室选课记录表course_selection字段类型说明idbigint主键student_idbigint学生idsection_idbigint教学班idselection_timedatetime选课时间statustinyint0-退课1-正常2-已删除这张表一定要加唯一索引(student_id, section_id)。否则同一个学生可以重复插入同一条选课记录后续统计就全乱了。最关键的是status设计成“状态标记”而不是直接删记录保留选课历史对日后查问题很关键。成绩表score字段类型说明idbigint主键student_idbigint学生idcourse_idbigint课程idscoredecimal(4,1)成绩semestervarchar(20)学期教师表、学期表不赘述学期表就是存当前学期名和选课起止时间方便系统判断“是否在选课时间段内”。2.3 容量与时间冲突的边界条件容量控制的逻辑大家都懂教学班的selected_count capacity就能选。但有个容易忽略的边界退课之后再选同一时刻只允许一个学生在一个教学班有一条status1的记录。还有开学第一周允许退课、之后锁定这种业务规则最好做成接口参数或配置文件不要写死在SQL里。时间冲突判断必须考虑跨周次的情况如果已选课程是1-8周新选课程是9-16周虽然都是周一第1-2节但实际并不冲突。所以不能用简单的“同日同时段”就判定冲突必须比较周次区间是否有交集。我写的判断SQL逻辑如下伪代码SELECT COUNT(*) FROM course_selection cs JOIN section_time st ON cs.section_id st.section_id WHERE cs.student_id #{studentId} AND cs.status 1 AND #{newWeekStart} st.week_end AND #{newWeekEnd} st.week_start AND st.day_of_week #{newDayOfWeek} AND #{newStartSection} st.end_section AND #{newEndSection} st.start_section这个条件本质上就是“两个区间相交”的判断新课程周次不早于已选课程的结束周、新课程结束周不晚于已选课程的开始周同时星期相同、节次区间相交。这样能覆盖部分重叠、完全包含、跨周等场景。3. 实操过程与核心功能实现3.1 微信登录与学号绑定流程小程序登录不是直接拿用户名密码而是通过wx.login()获取临时code后端拿这个code去微信接口换openid和session_key。然后后端自己生成一个业务token返回给前端。为什么不能直接把openid当token因为openid是敏感标识且固定不变如果小程序包被反编译存在本地存储里的openid会被冒用。用缓存token过期时间设为2小时刷新后重新签发安全得多。小程序端代码// 登录页 wx.login({ success: async (res) { const code res.code const result await request.post(/auth/login, { code, studentNo: this.data.studentNo, name: this.data.realName }) if (result.code 0) { wx.setStorageSync(token, result.data.token) wx.switchTab({ url: /pages/index/index }) } else { wx.showToast({ title: result.message, icon: none }) } } })后端处理PostMapping(/auth/login) public Result login(RequestBody LoginDTO dto) { // 1. 用 code 调用微信 auth.code2Session 接口 // 2. 根据返回的 openid 查询 student 表 // 3. 如果该 openid 还没绑定学号则用 dto.studentNo dto.name 绑定 // 4. 生成随机 token存入 redis 或内存缓存返回前端 }这里要注意顺序先用openid查学生如果查不到再让用户输入学号和姓名进行绑定。不要让用户直接注册成新账号否则一个学校可以出现几百个“张三”且无法管理。绑定后学号和openid就一一对应。3.2 选课接口的并发与事务控制选课是最容易出并发问题的地方。想象一下一个热门课程容量200人开选瞬间2000人同时点击“选课”如果代码写成UPDATE course_section SET selected_count selected_count 1 WHERE id ? AND selected_count capacity;MySQL会自动对这条更新加行锁只要判断条件为真才更新能挡住大部分超卖。但如果后面又有插入选课记录就不能保证原子性了必须用事务保证“更新计数”和“插入选课记录”同成功同失败。我更推荐方式是这样在事务中SELECT * FROM course_section WHERE id ? FOR UPDATE查询当前已选人数。判断selectedCount capacity不满足直接抛异常回滚。更新selected_count selected_count 1。插入选课记录。提交事务。用FOR UPDATE把该教学班的行锁住其他请求等待。注意必须保证事务的隔离级别至少是“读已提交”否则两个事务同时读到旧值会造成超选。在Spring Boot里加Transactional注解配合SELECT FOR UPDATE是稳的。核心代码片段Transactional public void selectCourse(Long studentId, Long sectionId) { CourseSection section courseSectionMapper.selectForUpdate(sectionId); if (section null) { throw new BusinessException(教学班不存在); } if (section.getSelectedCount() section.getCapacity()) { throw new BusinessException(该教学班已满员); } // 检查时间冲突 if (checkTimeConflict(studentId, section)) { throw new BusinessException(时间冲突请重新选择); } // 更新人数 courseSectionMapper.increaseSelectedCount(sectionId); // 插入选课记录 courseSelectionMapper.insert(new CourseSelection(studentId, sectionId, 1)); }selectForUpdate对应的Mapper写法select idselectForUpdate resultTypecom.example.entity.CourseSection SELECT * FROM course_section WHERE id #{id} FOR UPDATE /select实际测试过当并发量在每秒百次以内时这个方案完全没有问题对于课程容量通常几十到几百的高校选课场景足够。真要面对全校同时抢课的高并发得引入Redis预扣减和消息队列但毕业设计不用写到那一步逻辑和事务处理好已经能说明问题。3.3 课表的动态生成与前端展示课表展示有一个常见误区直接把后端查出来的数据平铺到页面上。正确做法是根据“周次”动态生成比如第一周周一第1节有课第二周周一没课。前端用一个二维数组[week][day][section]展示一个表格。我后端接口返回的是学生已选教学班的信息包括课程名称、教师、教室、周次区间、节次区间前端再裁剪成“1-16周的单双周”效果function isWeekActive(week, start, end) { return week start week end }然后渲染时循环20周每周的每一天、每个节次判断是否有课。如果某个教学班单双周不同比如单周上、双周不上可以用weekInterval字段表示间隔比如1-15表示每周1-13但步长为2表示单周。设计中加一个week_step字段就能支持。这是很多同学忽略的细节建议一开始就纳入设计。前端课表页面用view网格布局横向是“周一”到“周日”纵向是节次每节课用绝对定位的色块展示。对初学者来说不熟悉绝对定位的话用最简单的表格布局也够用只需保证数据准确。3.4 个人成绩查询与信息展示成绩查询相对简单就是按学期查选课记录和成绩表联表。需要注意如果学生选了课但还没出成绩成绩字段为null前端要显示“暂无”而不是空字符串或0。这里我踩过一个坑后端直接返回0前端显示0分不少同学来问“我是不是挂科了”。后端的空值判断不要省。3.5 教师端与管理端的轻量实现毕业设计时间有限教师端可以只做“查看教学班学生名单、录入成绩”。管理员端只做“开课、设定教学班时间地点、调整容量”。我当时的做法是复用同一套登录体系通过学生表的role字段区分角色小程序端根据角色显示不同页面。这个办法简单、不用单独做后台管理系统答辩演示时也够直观。如果你想要一个更完整的后台也可以单独用一个Vue管理端但那相当于又做了一个中大型前端项目。除非时间充裕否则建议控制范围。4. 常见问题与排坑实录4.1 request合法域名与真机调试验证本地调试时可以在开发者工具里勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”但真机预览就不行了。真机要求所有请求地址必须是HTTPS且在微信公众平台后台配置了request合法域名。毕业设计最常见的卡点是电脑上能跑手机上一请求就报错fail url not in domain list。解决办法申请一个云服务器绑定备案过的域名或者用已经备案的配好Nginx反向代理和SSL证书把后端服务挂上去。如果不想备案可以临时用云开发环境或者内网穿透做演示但答辩现场存在不稳定性最好有备案域名。我当时的做法是在后端接口测试阶段用IP端口调试把“不校验合法域名”勾上等最后部署阶段再切到正式域名。注意小程序体验版也受真实域名限制不能拿IP直接访问。4.2 微信授权弹窗与用户拒绝授权很多同学喜欢一进小程序就弹授权框要头像昵称结果用户拒绝后就死循环。正确做法是先让用户进入系统等到真正需要时才调用授权。我采用的策略是第一次打开显示一个“绑定学号”的引导页用户主动输入学号姓名绑定全程不强制弹任何授权框。头像和昵称通过button open-typechooseAvatar让用户主动触发而不是启动时自动弹。getUserProfile接口已经调整无法直接获取用户的头像和昵称用open-data又限制很多。所以最稳妥的方案就是让用户手动填写或选择头像。不要在这个问题上钻牛角尖选课系统根本不需要真实头像昵称。4.3 选课并发测试时发现超选我写完选课接口后用JMeter模拟50个并发同时选一个容量为10的教学班结果选出了13条记录。排查后发现是事务没有生效原因是我在同一个类内部的调用selectCourse(this, ...)绕过了Spring事务代理。解决办法是注入自己的Service代理或者把内部调用改成另一个Service调用保证事务注解生效。还有一个坑SELECT FOR UPDATE必须用在“事务内”才有效。如果外层没加Transactional那锁会在语句执行完的瞬间释放等于没有锁。我刚开始就是这样看了半天MyBatis日志也没发现问题。后来加了一层Service事务边界正确重新压测结果稳定在10条。4.4 时间冲突判断的周次重叠问题有同学反映“同一时间选了1-8周的高数和9-16周的大物系统判定冲突”。原因就是代码只比较了星期和节次忽略周次区间。上面给出的相交判断公式要写进SQL或Java代码里而不是用简单的等于比较。我当时的测试用例里专门加了几组边界数据已选1-8周新选9-16周应该不冲突已选1-8周新选8-16周应该冲突第8周重叠已选1-8周新选1-8周冲突已选1-8周新选2-7周冲突新课程被包含用自动化测试把这些用例跑一遍能有效避免答辩现场翻车。4.5 小程序包大小与图片资源优化小程序主包不能超过2MB超过就没法直接上传。选课系统本身不大但如果后端返回的课程图片或图标过多或者把图片Base64塞进WXML里很容易爆。我的建议是课程封面图和图标使用云存储或OSS链接不在包里放本地图。所有列表接口采用分页pageSize一般设20。公共组件代码及时抽取减少重复。编译时留意开发者工具右上角“代码包”一栏如果接近2MB优先压缩图片删除无用代码最后再考虑分包加载。4.6 缓存策略与请求封装为了让选课列表响应快我设置了wx.setStorage缓存课程分类基础数据每5分钟刷新一次。但要注意选课时不能用缓存的“已选人数”做任何判断必须实时请求后端。因为缓存数据是旧的页面显示“已满”其实是上次加载时的状态。我在前端把“查看详情”和“选课按钮”区分开点击选课时重新拉取最新接口。请求封装方面我写了一个request.js统一包一层const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: API_BASE url, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) }, success: (res) { if (res.data.code 401) { wx.navigateTo({ url: /pages/login/login }) reject(new Error(未登录)) } else if (res.data.code 0) { resolve(res.data.data) } else { wx.showToast({ title: res.data.message, icon: none }) reject(new Error(res.data.message)) } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) }统一处理token过期和网络错误项目联调时省下的时间非常可观。4.7 答辩演示前的三个检查项最后提醒几点答辩演示的坑手机预览时确保后端服务已部署到服务器且手机和服务器网络通畅。不要依赖本机localhost。演示前把“体验版”二维码提前准备好不要现场扫码等加载。数据要准备一个“有课表、有已选课程、有成绩”的演示账号避免现场注册新账号没有数据可看。我当时演示就吃过亏现场手机扫完码首页是空的因为测试账号忘了预处理。后来我写了一个initData接口传入学生学号自动给他预置几门课程和历史成绩演示前点一下就搞定。写在最后的一点点经验这个项目做完我最大的体会是选课系统的难点从来不在“功能多”而在“边界到底有没有想清楚”。容量、时间冲突、并发、登录态每一个点拆开都不难合在一起就考验工程思维了。如果你正在做这个课题尽量不要急着堆功能先花一天把表结构和业务规则理清楚比多写十个页面都管用。我自己在踩过超选和时间冲突的坑之后后来设计任何带状态的系统都会先梳理事务边界和唯一约束。这可能是毕业设计除了一篇论文之外最能留下来的一点东西。希望这篇记录能帮你少走几段弯路。
返回列表