
选课系统这种项目我在课设和毕设里见过太多版本了但绝大多数都是看起来能跑的演示品真正能拿去答辩、能演示完整业务闭环的其实不多。今天这篇就围绕一个基于微信小程序和Spring Boot的在线选课系统展开从需求拆解、表结构设计、核心接口逻辑到部署上线把一套能实际交付的源码讲透顺便把我踩过的坑也一并交代清楚。这个项目解决的是高校教务场景里最典型的痛点学生选课靠抢、教师排课靠Excel、管理员统计靠人工。用微信小程序做前端学生扫码即用不用装App后端用Spring Boot扛住选课高峰期的并发请求。整套系统适合三类人看正在做Java课程设计的大学生、准备毕业设计但时间紧张的应届生以及想入门小程序全栈开发、想搞明白前后端怎么对接的开发者。1. 项目全貌这套选课系统到底在做什么先花几分钟把业务边界捋清楚。很多人拿到源码第一步就急着跑起来结果数据库初始化脚本都没执行登录页面白屏半天其实问题大多出在没搞明白系统是给谁用的、每个角色能看到什么。1.1 传统选课的痛点与这个项目的解法在没做信息化之前很多学校的选课流程是这样的教务老师把课程表发到班级群学生用Excel填志愿班委汇总后交给老师老师再手工排冲突、调名额。这个过程最折磨人的是三个问题第一个是课程冲突没人提前预警学生会同时选两门时间重叠的课最后上课时才发现撞了时间第二个是热门课超员严重往往一个名额有几十个人报老师只能靠运气筛选第三个是退课换选特别麻烦每次调整都要重新走一遍邮件确认流程。这套微信小程序选课系统要解决的就是把上面这个流程全部线上化。学生端能看课程列表、按时间段筛选、一键选课退课、查看课表教师端能发布课程、设置容量、导出选课名单管理员端负责账号管理、学期切换、数据统计。核心逻辑是系统在选课环节自动校验时间冲突和容量限制数据库层面用唯一索引加事务保证不超卖前端小程序做到秒级响应。1.2 三种角色的权限边界与功能差异角色权限的设计是这类系统的地基权限没掰扯清楚后面加多少功能都是乱的。我按实际使用频率把三个角色的核心功能做了个划分角色核心功能数据操作范围学生浏览课程、选课、退课、查看个人课表、修改资料仅限本人选课记录教师发布课程、维护课程信息、查看选课名单、录入成绩仅限本人创建的课程管理员用户管理、学院/专业维护、学期切换、全局统计全部数据拥有最高权限这里有个设计上的细节值得注意教师的课程创建权限要不要管理员审核我见过两种方案一种是教师提交后直接生效简单但容易出错另一种是教师提交后管理员审核通过才开放选课多一步流程但可控性强。这个项目采用的是后者好处是管理员能在选课开始前统一检查课程数据完整性避免学生选到信息缺漏的课。如果不加这层审核万一教师忘记填上课地点学生就得满楼找教室。学生的权限边界更要严格卡死学生只能操作自己的选课记录不能看到其他学生的课表更不能修改课程容量。这些限制在后端接口层面就要做校验不能指望前端隐藏按钮就算完事因为小程序的request是可以被抓包的。2. 技术选型与架构设计为什么是微信小程序Spring Boot说实话光看微信小程序Spring Boot这个组合很多人第一反应是这是课设标配吧确实这个组合在高校项目里出现频率极高但频率高不代表它没道理。小程序天然适配校园场景学生不用额外装App微信扫一扫就能进Spring Boot则让后端开发效率拉满生态成熟遇到问题随便一搜就有解决方案。2.1 前后端分离架构的组装逻辑这套系统的架构属于典型的互联网分层架构没有微服务那些花架子单体应用足够撑起一个学期的选课需求。完整调用链路是微信小程序端发起HTTPS请求经过Nginx反向代理转发到Spring Boot后端后端用Spring MVC接收请求并做参数校验然后调用Service层处理业务逻辑Service层通过MyBatis操作MySQL数据库认证信息用JWT存续会话状态。有人会问为什么要用Nginx而不让小程序直接访问Spring Boot端口两个原因第一个是微信小程序对HTTPS证书要求很严格request合法域名必须是HTTPSNginx能统一做SSL卸载否则得在Spring Boot里配证书第二个是高峰选课时Nginx可以做负载均衡和静态资源缓存即使以后要扩展到多台后端服务器也不用改小程序端的任何代码。数据传输格式上目前主流做法是小程序端用wx.request发送JSON格式数据后端的Controller接收后统一封装为Result对象返回格式是{code: 200, message: success, data: {...}}。小程序端拿到响应后先判断code再处理data里的业务数据这套约定必须前后端都遵守否则联调时会出现各种莫名其妙的解析错误。2.2 数据库建模的关键表与关系设计整套系统最核心的表有四张用户表、课程表、选课记录表、学期表。很多新手设计表的时候会漏掉学期这个维度结果就是跨学期的数据混在一起查历史选课记录时全部乱套。这四张表的字段设计我按实际项目经验梳理一下。用户表的核心字段包括主键id、用户名、密码BCrypt加密存储、角色student/teacher/admin、姓名、学号/工号、学院id、专业id。角色字段用字符串更直观用数字枚举也行但要注意别在代码里写魔法数字最好定义常量类统一管理。课程表的核心字段包括主键id、课程名称、课程代码、授课教师id、学期id、上课时间JSON格式或字符串、上课地点、总容量、已选人数、学分、课程性质必修/选修、状态待审核/已发布/已结束。这里最关键的是上课时间和总容量这两个字段。上课时间我用的是JSON字符串存储比如[{week: 周一, period: 3-4节}, {week: 周三, period: 1-2节}]这样解析起来灵活数据库也不用来回拼接字符串。选课记录表的核心字段包括主键id、学生id、课程id、选课时间、退课时间、状态已选/已退。这四张表之间的关系是用户表和课程表是多对多通过选课记录表关联学期表作为维度表挂在课程表上。需要特别注意的是选课记录表要建联合唯一索引(student_id, course_id, semester_id)这是防止重复选课的最后一道防线。很多教程里会忽略这个索引导致高并发下学生重复提交选课请求时产生两条记录业务上又没法判断哪条是真的。2.3 认证与权限控制方案小程序的登录认证和普通网页登录有个本质区别小程序拿不到传统意义上完全由自己控制的 Cookie虽然能用wx.setStorage模拟但安全性不够。更通用的做法是使用 JWTJSON Web Token登录成功后把 token 存到小程序的本地缓存里每次请求在 header 中携带Authorization: Bearer token。在后端我实现了一个拦截器来处理每个请求的token验证拦截器核心逻辑是先取header里的token然后解析token获取userId和角色信息再放行请求让Controller继续处理。需要说明的是拦截器只能验证用户是否已登录至于登录用户有没有权限操作这个数据必须在Service层根据业务逻辑判断。举例来说学生A调用删除选课记录接口时即使传的是学生B的选课记录idService层也要校验当前登录用户和记录所属用户是否一致因为token里存的信息可以被伪造篡改不能盲目信任客户端传来的任何id。JWT的过期时间建议设成7天学期中间频繁登录很烦人7天基本能覆盖学生日常使用场景。如果后续要做记住我功能可以把过期时间拉长到30天但要注意安全性会相应降低。小程序端的token失效后后端返回code: 401小程序拦截到后跳转登录页重新授权这个链路要提前测一遍别等到答辩时才现找问题。3. 核心模块实操从接口到页面的完整落地前面把架构和表结构讲清楚了这一部分进入正题按业务优先级把核心模块过一遍。选课系统最敏感的就是选课和退课两个操作这两块逻辑写不对其他功能做得再花哨也白搭。3.1 选课主流程并发扣减与冲突校验选课接口是整套系统并发压力最大的接口热门课一开放选课就可能涌入几百上千个请求。选课的核心业务流程分三步第一步校验课程状态是否已发布第二步校验学生是否已选过该课程第三步校验课程容量是否已满第四步校验时间是否与其他已选课程冲突最后执行插入选课记录并更新课程已选人数。校验和插入这两步之间天然存在并发窗口。举个例子一门课容量只剩1个名额两个学生同时发起选课两个请求都通过了容量判断结果数据库里插入了两条记录而课程容量变成负数了。解决这个问题的最经典做法是在update语句的条件里带上已选人数 总容量UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count total_capacity这条SQL执行后返回影响行数如果返回0说明容量已满选课失败如果返回1说明扣减成功再执行插入选课记录。用数据库层面的原子操作来兜底比在代码里加锁更可靠。这里最关键的一步是更新课程人数和插入选课记录必须在同一个事务里执行否则会出现选课记录插进去了但人数没加上去的幽灵数据。时间冲突校验的逻辑其实不复杂先把学生已选的所有课程时间字符串解析出来和当前待选课程的时间字符串做两两比对判断是否有重叠。我在项目里写了一个时间解析工具类把周一3-4节这种格式解析成具体的周几和节次集合然后用集合的contains方法判断是否冲突。这个方案性能足够每学期选课记录不会超过20条遍历一次的耗时可以忽略不计。3.2 退课与换选的边界处理退课的逻辑比选课简单但有一个边界条件特别容易忽略选课截止时间。很多学校会规定选课截止日期过了这个时间就只能加课不能退课。实现方案是给课程表加一个end_time截止时间字段退课接口进入后先判断当前时间是否在截止时间之前不在则直接返回已过退课截止时间。退课的SQL事务包含两步从选课记录表删除记录课程表的已选人数减1。这两步同样需要在同一个事务中执行顺序颠倒会出现人数对不上的数据问题。有同学追问为什么要先删除记录再减少人数、而不是反过来其实只要两条SQL都在同一事务里谁先谁后不影响最终结果关键还是事务不能拆散。不过实际写法我习惯先删选课记录再更新课程人数因为删除操作如果失败后续步骤就不必执行。换选课的业务其实就是退课加选课的组合操作但要注意的是不能拆成两个独立请求让学生自己点要提供一个replaceCourse接口内部先退旧课再选新课并且整体放在一个事务里。这样做的目的是防止先退后选两步之间课程被其他人抢走导致学生两头落空。虽然事务不能锁住其他学生选课这个操作因为那是别人的事务但至少保证了同一个学生操作的原子性。3.3 管理端课程发布与名额控制管理员端的课程发布流程我设计成了教师提交、管理员审核的两段式。教师在教师端创建课程时状态默认为PENDING此时学生在小程序端看不到这门课。管理员在管理后台通过审核后状态更新为PUBLISHED学生才能检索和选课。这个设计有一个隐藏的好处管理员可以批量审核课程并在审核通过的同时给课程设置统一的选课开放时段。比如管理员下午2点批量通过20门课这些课随即进入可选的开放状态学生2点整就能选这比教师每门课各自上架更可控碰到高峰也不用担心课程陆续出现导致学生反复刷新。名额控制上管理员可以对每门课单独修改总容量我在修改接口里加了一个限制修改后的容量不能小于当前已选人数。这听起来是常识但真的有人把容量从60改成50而课程里已经选了55个学生导致数据库出现脏数据。3.4 小程序端页面的关键交互小程序端我用的是原生微信小程序框架没有引入uni-app或Taro原因很简单原生框架足够应付这类表单列表型应用而且编译调试快不用踩跨端框架的边界问题。页面结构上核心页面有五个登录页、课程列表页首页、课程详情页、我的课表页、个人中心页。课程列表页是整套系统的门面学生打开小程序第一个看到的就是它。这里有一个交互细节值得品课程列表不要一次全部加载要按条件筛选。我在列表页顶部放了学期选择器、课程性质筛选框、关键字搜索框配合后端的分页接口每页10条数据用onReachBottom实现上拉加载更多。分页参数用pageNum和pageSize返回格式是{total, list}total给前端算总页数用。我的课表页用了一个很笨但好用的小技巧按周一到周日固定七行把已选课程的节次映射到对应行里左侧显示第1-2节、3-4节这些时间段右侧用颜色块标出课程名称。这个页面其实不需要任何复杂组件纯用 CSS Flex 布局加wx:for循环就能画出满意的课表关键点是课程时间解析要在后端做好小程序端只负责渲染不要在setData里做大量的字符串切割运算会卡顿。4. 踩坑实录常见问题与排查技巧这部分按真实开发中遇到的高频问题整理从后端到前端再到底层数据能给正在写代码或者调试的你省不少时间。4.1 跨域拦截与请求头丢失小程序发请求不存在浏览器跨域的问题但开发调试时如果你用微信开发者工具的本地模式且后端接口没跑在DNS上会碰到https://域名白名单不过的报错。解决办法是在微信开发者工具中勾选不校验合法域名但要注意这只是开发期的临时方案真机预览时必须配置HTTPS域名并到微信公众平台添加白名单否则手机上打不开接口。后端跨域问题的重灾区其实是自定义Filter或拦截器里对OPTIONS请求的处理。如果你在小程序端自定义了header比如Authorization后端在CORS配置里必须显式声明allowedHeaders(*)或者把Authorization加进去否则部分网络框架会拦截掉预检请求前端表现为请求失败而不是401。4.2 登录态失效与token刷新小程序端的token过期是最常见的隐性故障表面上看是用户突然被登出实际是后端拦截器返回401后小程序端没有统一处理这个状态码而是直接展示错误提示。我的建议是在小程序端的request工具函数里做全局拦截如果响应码是401清空本地缓存跳转到登录页重新授权。这个处理要在封装层解决不能在每一个具体请求里重复写否则代码会非常碎片化。后端做定时登录态清理时也能节省大量存储空间。4.3 并发选课超卖问题超卖问题的根源是检查与扣减分离代码里先用select查了一波数量再执行update扣减两步之间产生了空隙。我曾经把容量校验和更新写成了两步操作测试时单线程没事一到并发就出问题用Jmeter压了50个并发线程超卖率几乎100%。后来改成前面提到的条件更新把selected_count total_capacity放进update的where条件里压测数据立刻归零。这个问题的解法思路是通用的凡是涉及库存扣减的场景都可以套用。4.4 前后端时间格式不一致时间格式的问题看起来鸡毛蒜皮真遇到了能让你排查一晚上。后端返回的时间类型默认是yyyy-MM-dd HH:mm:ss但如果用JsonFormat没配好可能会变成一串时间戳数字。小程序端对new Date(时间戳)的处理没问题但如果你把字符串直接渲染到页面上展示的就是难以阅读的格式。我的方案是后端统一返回标准字符串格式在实体类的日期字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)避免小程序的时区偏移导致页面时间显示差8小时。5. 从源码到上线部署步骤与扩展建议很多同学拿到源码后第一句话就是怎么跑起来这一部分我把从零到能访问的完整步骤列一遍顺便把部署到服务器上的注意点讲透。5.1 本地环境快速跑通跑通这套系统需要的软件环境如下JDK 1.8及以上、Maven 3.6、MySQL 5.7或8.0、微信开发者工具最新稳定版、Nginx可选本地调试不需要。第一步创建数据库并导入项目里的sql目录下的初始化脚本里面有建库建表和默认数据的SQL默认管理员账号密码在脚本里有标注。第二步修改application.yml里的数据源配置把数据库地址、账号、密码改成你自己的。第三步在项目根目录执行mvn spring-boot:run看到端口启动成功的日志就说明后端起来了。第四步用微信开发者工具导入小程序端目录在app.js或utils/config.js中把接口请求地址改成http://localhost:8080如果开发工具提示域名不合法在详情勾选不校验合法域名。最后一步编译小程序用管理员账号登录管理后台审核发布几门课程再用学生账号登录小程序选课流程基本就能跑通了。这里有一条重要提示默认数据库中的数据是配套好的不要随意删减核心表的数据否则联调时容易缺字段。很多初学者一上来就把管理员账号删了结果后台进不去只能重置数据库。5.2 服务器部署注意点如果要把项目部署到云服务器上有几个点比本地跑通更需要注意。MySQL数据库要设置合理的编码建库时指定utf8mb4否则课程名称里的特殊字符会乱码。Spring Boot打的jar包执行时需要指定环境变量或外部配置文件建议用java -jar xxx.jar --spring.profiles.activeprod的方式隔离生产配置不要把密码硬编码在代码里。小程序端发起的HTTPS请求需要域名备案并且域名必须配置SSL证书这是上线前最大的一个坎。部署到服务器后还有一个容易忽略的问题防火墙和安全组策略默认要放行8080端口否则外部请求全部超时。我用Nginx做反向代理时把/api/前缀的请求转发到Spring Boot的8080端口小程序端实际请求的是https://你的域名/api/xxx这样后端服务对外完全屏蔽也能统一处理静态资源缓存和Gzip压缩。5.3 后续扩展方向源码基础上想加亮点功能的话优先级最高的有三个方向第一是消息推送选课成功后通过小程序的订阅消息向学生推送通知需要到微信公众平台申请消息模板并配置模板ID第二是课表导出可以把已选课程导出成iCal格式方便学生导入到手机日历这个功能在答辩时特别容易引起老师兴趣第三是选课时间分批次开放比如研究生和本科生分批开放选课在课程表里加一个select_start_time字段就能实现学生端到点才能看到该课程的可选状态。我个人在实际操作中最大的体会是这类系统真正的难点往往不在于某个技术栈多深奥而在于把业务边界想清楚表结构能不能撑住需求变化接口设计有没有预留扩展点。拿到源码后别急着炫技改架构先把选课流程全线走通观察数据在每个环节的变化理解每个字段存在的必要性然后再谈优化和重构。你在跑通流程过程中踩到的每一个坑都会比抄十遍代码更值钱。