这个项目我拿到手里看了一遍之后,第一反应是:它其实是"智慧校园"这一类系统里最适合拿来练手和做毕设的选题之一。学生选课这个业务足够经典,需求边界清晰,但又覆盖了用户登录、权限管理、课程库存、并发控制、信息展示、移动端交互这些主干技术点,一套做完,等于把Java后端和小程序前端的主流程都过了一遍。更关键的是,它有一个非常具体的实际场景——高校选课。
这类项目真正难的不是某一个技术点,而是把"学生、教师、管理员"三条角色线梳理顺,把"课程管理、选课抢课、课表生成、成绩录入"这条业务链路跑通,再配合微信小程序端的操作体验,整体做完整。下面我会从项目拆解、技术选型、数据库设计、核心实现到部署避坑,把我做这套系统时的完整思路和处理方式展开说说。
1. 为什么是"智慧校园+选课":需求边界与项目价值的判断逻辑
1.1 从业务场景看选课系统的核心诉求
一开始就有个很现实的问题需要先想明白:这个系统到底要解决什么?是和大型在线教育平台竞争,还是做一个贴合高校实际管理模式的工具?我当时的判断是后者。高校选课场景里,真正的痛点集中在三处。
- 高峰期的选课压力。全校几千上万名学生同时登录系统抢课,普通架构很容易出现接口超时、数据库连接打满的问题。
- 信息不透明。学生想知道"这门课还有没有名额""我选的课时间冲突没有",老师想快速看到"选我这门课的有哪些人",管理员则需要统一掌握所有课程的开课情况,这些需求在没有系统之前基本靠Excel和人工沟通。
- 客户端体验。现在学生群体几乎人人都有微信,比起让每个学生单独下载一个App,微信小程序的获客和使用成本低得多,用完即走,不需要单独安装,也不占用手机内存。
这些背景决定了项目的产品形态:微信小程序作为学生端和教师端的轻量入口,SpringBoot作为服务端提供业务支撑,管理员通过Web端(或小程序内嵌管理模块)做整体管控。
1.2 为什么这种项目适合学习/毕设/面试
从学习价值看,选课系统天然包含"账号体系+权限控制+业务状态流转+数据统计"四个模块。你做完之后,其实就掌握了一套基于角色的权限设计通用方案,这套方案放到任何管理系统里都能直接复用。
从面试角度说,选课系统很容易引出几个有意思的问题:你怎么防止超选?怎么处理选课高峰的并发?怎么设计课程表的数据模型?这些问题比单纯背诵Spring Boot注解实用得多,面试官一听就知道你是真做过还是只会看教程。
还有一个很现实的点:选课系统涉及的数据关系(学生-课程-成绩)是非常典型的多对多关系,数据库设计的思路清晰,就很好讲清楚。换成其他偏展示类的系统,反而不好体现设计能力。
1.3 项目整体形态和交付内容
标题里提到"源码+文档+运行视频+讲解视频",这类交付组合其实很符合当前学习者的真实需求。源码是基础,文档解决"系统怎么设计出来的"的问题,运行视频解决"我买回来该怎么跑"的起步障碍,讲解视频则负责把关键代码逻辑掰开揉碎讲明白。对学习者来说,最忌讳的是打开源码一头扎进去看,先跑起来,再对照文档,最后看讲解,这个顺序会更有效率。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型不能拍脑袋:SpringBoot、微信小程序与配套组件的组合逻辑
2.1 后端主体为什么锁定SpringBoot
现在做Java后端,SpringBoot基本是事实标准。它的意义不只是"简化配置"这么简单,而是把Web开发里那些繁琐的重复劳动降到了最低。你写一个REST接口,只需要几个注解,启动内嵌Tomcat,打个Jar包就能跑,这对项目周期紧张、需要快速交付的场景非常友好。
具体选版本的时候要注意一点:SpringBoot 2.x和3.x差别不小,3.x要求JDK17起步,2.x常用JDK8。我自己做这个项目时用的是SpringBoot 2.7.x + JDK8组合,因为大多数高校机房和培训环境的JDK版本还停留在8,踩坑成本低,网上资料也多,跑起来不容易被环境问题卡住。如果你本机装的是新版JDK,也可以直接上SpringBoot 3.x,但配套的MyBatis等组件版本要选兼容版本,不然会有一堆兼容性报错。
2.2 数据持久层:MyBatis-Plus还是JPA
在持久层框架的选择上,很多人会纠结用Spring Data JPA还是MyBatis。我的建议是:如果是自己从零写一套这种规模的管理系统,直接用MyBatis-Plus。
原因有几个:
- 单表CRUD不需要手写SQL,继承BaseMapper就自带增删改查方法,开发效率明显高。
- 选课系统里会频繁用到条件查询(按课程名称、按教师、按学分筛选),MyBatis-Plus的LambdaQueryWrapper写起来非常顺手,也不会出现字符串硬编码。
- 复杂多表查询(比如查学生已选课程详情)可以手写XML里的SQL,和简单查询区分开,后期维护清晰。
对比一下,JPA在简单场景下够用,但碰到多表关联、动态条件就比较绕,尤其是对SQL不是特别熟练的同学,问题排查时容易一头雾水。MyBatis-Plus的调试思路很直白:看控制台打印的SQL就行。
2.3 认证与权限方案:JWT而非Session
小程序端天然有个特点:它不是浏览器环境,没有传统Cookie/Session那一套会话机制。你封装一个请求函数,每次发请求都主动在Header里带token字段,后端再做token校验,这种无状态认证方案配合小程序非常自然。
我引入的是JWT(JSON Web Token)。流程是:
- 登录凭证(wx.login拿到的code)发给后端,后端调微信接口换取openid。
- 再用openid去用户表查是否有这个账号,有就直接签发token,没有就先注册再签发。
- token里可以塞userId、role(学生/教师/管理员)这些基础身份信息。
- 后续请求前端请求拦截器统一往Header里塞Authorization字段,后端写一个拦截器或AOP做token解析和角色校验。
这里有个经验:不要在小程序端存太多用户资料,只存token和必要的昵称头像就行,每次需要完整的用户信息时调后端接口获取,避免数据不一致。
2.4 缓存中间件:Redis用来解决抢课热点
如果做的是单机版Demo,完全可以不引入Redis,先保证项目能跑通。但如果想把选课高峰时期的问题考虑进去,Redis就有价值了:
- 用Redis做课程剩余名额的预扣减。学生点选课时,先对Redis里的课程名额执行decr操作,如果返回的值大于等于0,再执行数据库层面的正式选课;否则提示"名额已满"。这能挡掉绝大多数直接打在数据库上的无效请求。
- 用Redis存课程列表缓存。首页展示的开课列表如果每次请求都查MySQL,压力很大,课程列表变动又不频繁,完全可以缓存5分钟。
当然,Redis也不是银弹,如果项目定位是快速跑通演示流程,初期先用数据库+事务控制选课,等基础版本稳定后再把Redis加进来做优化,也是合理路径。我在后面"并发控制"那一节会更详细说这个取舍。
2.5 微信小程序前端的技术构成
小程序端直接使用微信官方原生开发方式——WXML + WXSS + JS + JSON配置文件。选原生不是因为它多先进,而是因为微信生态的文档、社区资料基本都是围绕原生写的,你遇到问题去搜索引擎也能最快找到答案。第三方框架(如Taro、uni-app)适合跨端需求,但这个场景只需要微信端,没必要引入额外编译层,反而增加不确定因素。
小程序页面规划方面,我建议按角色拆分tabBar:
- 学生端:首页(课程推荐/轮播)、选课中心、我的课表、个人中心。
- 教师端:我的课程、选课学生名单、成绩录入。
- 管理员端:用户管理、课程审核、数据统计(这部分也可以放到Web端做)。
实际开发中,管理员端想全部塞进小程序会让小程序包体积膨胀、操作也不够方便,所以很多项目会把管理员后台做成Web页面,小程序只承担学生和教师的高频操作。
3. 数据库设计是地基:学生、课程、选课三张核心表如何建模
3.1 角色与用户表的边界
很多初级设计会把student、teacher、admin拆成三张独立的表,然后每张表都存一样的nickname、avatar字段。这套设计有问题:公共信息重复存储,扩展角色时需要重复改表。
更推荐的做法是一张sys_user表统一存储用户基础信息(userId、openid、nickname、avatar、role、createTime),再用role字段区分身份。如果某个角色有特殊属性(比如学生的学号、教师的工号),再分别设计student_info和teacher_info做一对一补充表。这样用户登录验证只用查一张表,维护成本也低。
具体字段建议:
- sys_user:id、openid、username、password(管理员后台登录用)、nickname、avatar、role(1学生/2教师/3管理员)、status、create_time。
- student_info:id、user_id、student_no、class_name、grade、major。
- teacher_info:id、user_id、teacher_no、title。
3.2 课程信息表的设计要点
课程表是整个系统的信息中枢,字段要能覆盖展示、筛选、统计三类需求:
- id、course_name、course_code(课程编号)、teacher_id(关联教师表)、credit(学分)。
- course_type(课程类别:必修/选修/公共课)。
- total_storage(总容量)、selected_count(当前已选人数)。
- class_week(上课周次,如1-16周)、class_day(星期几)、class_section(第几大节)、class_location(上课地点)。
- course_desc、course_pic、status(0草稿/1已发布/2已关闭)。
细心的朋友会发现,我还设计了selected_count字段。这个字段虽然冗余,但在列表页能直接展示"已选/容量",不需要每次实时count选课表,性能更友好。它的维护靠选课/退课事务里同步更新,保证不出现脏数据。
3.3 选课关系表:多对多关系的落点
学生和课程是多对多关系,必须有一张中间表来承接,也就是选课表。选课表的设计要遵循"一个学生选同一门课只能有一条有效记录"的原则,所以在数据库层面要加唯一索引。
选课表(course_selection)字段:
- id、student_id(关联用户表)、course_id(关联课程表)、select_time(选课时间)、status(0正常/1退课/2已结课)、score(最终成绩,教师录入)。
唯一索引非常重要:UNIQUE KEY uk_stu_course (student_id, course_id)。没有这个索引,即使后端代码里做了"判断是否已选",高并发下也可能出现重复插入。数据库的唯一索引是最后一道锁,前端和后端都要配合设置。
3.4 辅助表:公告、轮播图与操作日志
为了让系统看起来更"完整",我还会加上公告表和操作日志表:
- notice:id、title、content、publish_status、create_time。用于系统发布选课通知、放假安排等。
- operation_log:id、user_id、operation_type、operation_detail、create_time。记录用户关键操作,便于排查问题。
日志表看起来不起眼,但真正部署后非常有用。比如某位学生反馈"我明明选上又说我选了另一门课",有日志就能还原当时的操作链路。
4. 核心实现细节:从登录鉴权到并发抢课的完整链路
4.1 微信小程序登录流程:code换openid
小程序登录和后端对接是整套系统第一个"卡人"的点,流程本身不复杂,但顺序容易搞混。标准流程是:
- 小程序端调用wx.login(),获取一个临时code。
- 小程序把code发给后端(比如POST /api/auth/wx-login)。
- 后端拿code + appid + secret,调微信官方接口 https://api.weixin.qq.com/sns/jscode2session,换取openid和session_key。
- 后端拿openid查用户表,如果用户不存在就自动注册,然后生成JWT返回给小程序客户端。
这里有几个容易踩的坑:
- code只能用一次,而且有效期只有5分钟,不能缓存复用。
- 微信接口返回的openid是用户在该小程序下的唯一标识,同一用户的openid在不同小程序下是不同的,所以最好不要拿openid去跨平台匹配。
- appid和secret要放在后端,不能写在小程序代码里,否则谁都能拿到你的接口凭证。
前端请求封装时,建议用wx.request外面再包一层Promise,统一处理token注入、错误码提示等逻辑。
4.2 选课事务控制:保证数据一致性
选课的核心动作是"插入一条选课记录 + 课程已选人数加一",这两个操作必须放在一个数据库事务里。Spring里最简单的方式就是@Transactional注解。但要注意,光加注解还不够,还要考虑锁的问题。
先说不加锁会怎样:学生A和学生B同时选同一门只剩1个名额的课,两个请求都读到selected_count=49(课程容量50),都判断还有名额,都执行插入,最终selected_count变成51,超选了。
解决办法有几种:
- 悲观锁:SELECT ... FOR UPDATE,把选中的课程行锁住,其他事务必须等当前事务提交后才能操作。实现简单,数据绝对安全,但并发性能差,适合低并发场景。
- 乐观锁:在课程表加一个version字段,更新时SET selected_count = selected_count + 1, version = version + 1 WHERE id = ? AND version = ?,如果影响行数为0就说明版本冲突,提示重试。性能好,适合读多写少的场景。
- 唯一索引兜底:即使上面两步判断被绕过,重复的(student_id, course_id)也会被数据库唯一索引拦截。
我最终采用的是"数据库更新操作原子化 + 唯一索引兜底",核心SQL是:
sql复制UPDATE course
SET selected_count = selected_count + 1
WHERE id = #{courseId} AND selected_count < total_storage
如果update影响行数为0,说明没有名额了,直接抛业务异常。如果影响行数为1,再执行选课记录的insert。整个过程放在事务里,事务提交前持有行锁,天然防止了超选。这比先select再update要安全,也没有额外的锁等待风险,算是一个比较好的平衡方案。
4.3 节流与削峰:小程序端的体验优化
后端再稳,如果前端不做保护,抢课瞬间几十个请求依然会很吓人。我在小程序端加了两个简单限制:
- 选课按钮点击后置灰1秒,防止学生短时间内疯狂点。
- 同一门课重复点击时直接提示"处理中请勿重复提交",不发起新请求。
后端侧还可以用简单的手写限流(比如用Redis INCR记录同一userId在1秒内的请求次数,超过阈值直接拒绝)。这个功能在大规模部署时是必须的,但做学习项目时可以先不做,理解原理即可。
4.4 文件上传与静态资源处理
头像上传、课程封面图上传是小程序的常见需求。小程序里用wx.chooseMedia选图后,把图片文件通过wx.uploadFile传给后端,后端用MultipartFile接收,然后保存到服务器本地目录或者对象存储。为了在演示环境快速跑通,我通常直接保存到本地磁盘,然后配置一个映射路径让图片可以通过URL访问。
java复制@PostMapping("/api/upload")
public Result uploadFile(@RequestParam("file") MultipartFile file) {
String fileName = UUID.randomUUID().toString() +
file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
String path = uploadDir + fileName;
file.transferTo(new File(path));
return Result.success("/files/" + fileName);
}
注意,本地上传方案生产环境不推荐,服务器重启文件可能丢失,需要定期备份;更稳妥是接入云存储。
4.5 课表生成与冲突校验
"我的课表"是学生端使用频率最高的功能之一。实现方式不复杂:根据当前学生的选课记录,联查到课程表信息,按"星期几"和"第几节"两个维度,组装成一个二维数组返回前端,前端用表格组件渲染。
更关键的是选课前的"时间冲突校验"。同一学生不能选两门上课时间重叠的课,否则期末必炸。校验逻辑可以在事务里完成:查出该学生已选课程列表,判断新课的(class_day, class_section)是否和已有课程重合。这里要注意,有些课程是单周上课、双周上课的(周期隔离),需要结合class_week做精细化判断,但初版可以统一按"按周次重叠才算冲突"处理。
5. 部署与运行:那些能卡住你一下午的环境问题
5.1 微信小程序合法域名与HTTPS要求
小程序正式上线要求所有网络请求必须是HTTPS,且域名必须在小程序管理后台配置到request合法域名里。本地调试时可以勾选"不校验合法域名",但一旦发布,没有合法域名配置的接口根本请求不通。
这个环节有人会卡很久。我的建议是:如果你只是想跑通演示和学习,使用本地局域网调试即可;如果要正式发布,需要一台带备案域名的服务器,并配置SSL证书。很多学生问"为什么真机预览请求失败",十有八九是没开"不校验合法域名"选项,或者后端监听地址写成了localhost,正确做法是写电脑的局域网IP。
5.2 SpringBoot打包与服务器部署
SpringBoot项目开发环境直接运行main方法,部署环境用Maven打Jar包:
bash复制mvn clean package -DskipTests
java -jar target/campus-system.jar
更规范一点还可以用Docker部署,Dockerfile大概是这样:
dockerfile复制FROM openjdk:8-jdk-alpine
COPY target/campus-system.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
不过作为学习项目,直接java -jar跑起来其实就已经足够,Docker属于锦上添花。要注意的是服务器内存如果很小(比如1G),JVM参数要限制一下初始堆内存,否则OOM会让人怀疑人生。
5.3 数据库初始化与数据填充
项目里建议提供一个schema.sql和data.sql,把建表和基础测试数据都准备好。常见的坑是中文乱码,解决办法:建库时指定utf8mb4,连接串加上characterEncoding=utf8。
sql复制CREATE DATABASE campus_system
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
另外,开发阶段可以写一个CommandLineRunner或启动时执行SQL的机制自动初始化数据,省去手动导入的麻烦。
5.4 真机调试中的网络问题
如果小程序端用局域网IP访问后端,真机调试时必须保证手机和电脑连的是同一个WiFi。同时Windows防火墙默认会拦截外部访问,需要在防火墙里放行后端端口(比如8080)。这个坑非常典型:模拟器里一切正常,换真机就全部超时。
我自己的习惯是在后端统一开启CORS配置,避免前端请求跨域报错。虽然小程序本身不受浏览器同源策略约束,但如果有Web管理端,CORS就必须配置好。
java复制@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(Registry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*");
}
};
}
}
6. 让项目"活"起来:课程搜索、教师端录入与通知联动
6.1 多渠道课程搜索与筛选
选课列表页需要一个搜索框,支持按课程名、课程编码模糊搜索,同时支持按课程类别(必修/选修)、按学分范围筛选。后端使用MyBatis-Plus可以实现很干净的动态查询:
java复制public Page<Course> queryCoursePage(CourseQuery query) {
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(query.getCourseName()), Course::getCourseName, query.getCourseName())
.eq(query.getCourseType() != null, Course::getCourseType, query.getCourseType())
.ge(query.getMinCredit() != null, Course::getCredit, query.getMinCredit())
.le(query.getMaxCredit() != null, Course::getCredit, query.getMaxCredit())
.orderByDesc(Course::getCreateTime);
return courseMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()), wrapper);
}
注意模糊查询不要用"%"+keyword+"%"直接拼SQL,防止SQL注入,用MyBatis-Plus的条件构造器默认就是预编译,安全又简洁。
6.2 教师端成绩录入与发布
教师在小程序端查看选课名单后,可以按学生录入成绩。这一块要注意两个状态流转:
- 课程未结束前,成绩应为空;课程结束后教师才能录入。
- 成绩录入后学生端才能看到,录入状态要有明确标识。
成绩表我选择放在course_selection表里新增score字段,这样避免再拆一张成绩表增加关联复杂度。查询成绩单时,直接查选课结果列表即可。
6.3 公告通知与选课状态提醒
小程序首页除了课程推荐,还会滚动展示公告栏。后端提供公告CRUD接口,小程序端定时请求最新公告展示。选课成功后可以弹窗提示"选课成功",退课操作则要求弹窗二次确认,防止误操作。
7. 学习这套项目的最佳路径:从"能跑"到"能讲"
7.1 拿到源码后的启动顺序
很多人打开一个陌生项目的第一反应是乱翻代码,效率很低。我的建议是严格按下面的顺序走:
- 先看README和项目结构文档,搞清模块划分和启动方式。
- 导入数据库脚本,确保库表齐全。
- 启动后端,用Postman或Apifox测几个关键接口,确认登录、课程列表、选课接口返回正常。
- 用微信开发者工具导入小程序前端,改掉接口baseUrl,跑通一次完整选课流程。
- 最后再回头逐模块阅读代码,对照讲解视频理解设计思路。
7.2 如何把项目经验转化为面试资本
做完项目之后,一定要能回答清楚几个"为什么":
- 为什么选JWT而不是Session?(小程序无Cookie,无状态扩展性好)
- 为什么选课要做事务和唯一索引?(防止超选和重复选课)
- 如果全校2万学生同时抢课,你会怎么优化?(Redis预扣减、消息队列削峰、数据库行锁)
- 为什么用户表要分角色设计?(公共字段复用,扩展性好)
能把这些逻辑讲清楚,比单纯说"我做了一个选课系统"有说服力得多。
8. 项目扩展的四个方向
系统做完能跑起来只是第一步,想让它更有竞争力,可以从这几个方向扩展:
- 引入消息队列(如RabbitMQ)做选课请求异步化,削峰填谷,让选课系统能扛住真实高峰流量。
- 增加基于Redis的分布式限流和布隆过滤器,对抗恶意请求和无效选课。
- 用ECharts做管理员端的数据可视化,展示各学院、各年级的选课率、热门课程排行。
- 加入课程评价体系,学生选课后可以对课程和教师评分,形成一个反馈闭环。
这些方向都属于"在已有骨架上加肉",代码结构不变,但系统的完整度和你的技术深度会有明显提升。
从实际使用体验来说,这类选课学习项目最忌贪多贪全。先把主链路——登录、浏览课程、选课退课、查看课表、成绩查询——做扎实,再考虑优化和扩展。主流程跑通,你自然就知道下一步该往哪里发力了。
