1. 项目整体设计与思路拆解
1.1 这个智慧校园系统到底解决了什么问题
先说结论:这是一个典型的SpringBoot + 微信小程序前后端分离架构的校园选课学习系统。如果你正在准备毕业设计、课程设计,或者想独立接一个校园类的小程序项目,这套东西的参考价值很直接。
我拆解标题里的几个关键词:
- Java SpringBoot:后端主框架,负责业务逻辑、接口提供、数据持久化。
- 微信小程序:前端载体,学生和教师通过微信就能直接使用,不需要下载App,用完即走。
- 智慧校园:这是一个包了很多子模块的总称,核心落点通常在选课、课程学习、成绩、通知这些高频场景上。
- 学生选课程学习:本项目的核心业务闭环,学生登录 → 浏览课程 → 选课 → 学习课程内容 → 提交作业/考试 → 查看成绩。
- 源码+文档+运行视频+讲解视频:这是配套交付物,说明它是一个完整的、可运行、可答辩的教学型项目。
我见过不少类似的项目,真正做得好的不多,很多都卡在“业务逻辑简单、接口设计混乱、前后端对接困难”这几个问题上。这个项目的定位很明确:不是一个生产级的高并发系统,而是一个逻辑完整、能跑通、能讲清楚的教学型系统。如果你是学生,你要的不就是“能演示、能答辩、能讲清楚”吗?这就够了。
1.2 为什么选SpringBoot + 微信小程序这套组合
很多同学纠结过技术选型,我直接说我的看法。
后端选SpringBoot的理由:
- 生态成熟,资料多。搜“SpringBoot 选课系统”能搜出一堆参考,踩坑时找解决方案容易。
- 内置Tomcat、整合MyBatis-Plus、Spring Security、Redis都极方便,适合快速搭建RESTful API。
- Java的工程化程度高,代码结构清晰,答辩时讲架构比讲PHP原生或者Servlet顺手得多。
- 你们学校如果Java是主修语言,毕业设计用Java顺手能省不少事。
前端选微信小程序的理由:
- 不需要单独开发安卓和iOS两个客户端,一套代码两端跑。
- 学生群体本身就是微信重度用户,不用额外装App,扫码即用,场景契合度高。
- 小程序开发工具的调试、预览、上传都很方便,UI组件库(如Vant Weapp、ColorUI)拿来就能用。
- 运维成本低,不需要考虑应用商店审核,校内推广快。
我见过有人用Vue写Web端然后套个壳子应付,结果答辩时老师问“你这手机端体验怎么这么像网页?”直接尬住。微信小程序版本至少从形态上就是正规的移动端应用,形态对了,基础分就有了。
提示:选课系统这个选题,每年毕业设计都有十几个做,能不能拿高分,关键不在选题,而在你能不能让系统“讲出故事来”——业务流程闭环、角色权限清晰、异常处理周到,这些才是拉开差距的地方。
2. 核心功能模块与数据库设计详解
2.1 角色权限与登录设计
这个系统里我建议分三类角色,别贪多,三类够用且答辩时长逻辑清晰:
| 角色 | 核心职责 | 典型操作 |
|---|---|---|
| 学生 | 选课、学习、考试 | 浏览课程列表、选课/退课、观看课程内容、提交作业、查看成绩 |
| 教师 | 课程管理与教学 | 发布课程、上传课件、布置作业、批改打分、发布成绩 |
| 管理员 | 系统维护与基础数据 | 管理学生/教师账号、配置学期、审核课程、统计选课数据 |
登录这块我建议直接走微信的wx.login()获取code,后端拿code调微信接口换openid。每个用户的openid是唯一的,直接作为用户标识。再配合一个userId做业务关联就行。
但要注意,真实生产环境不能只靠openid裸奔,这只是一个演示项目的基本做法。更好的做法是:
- 小程序端
wx.login()拿到code。 - 后端用
code+appId+secret请求微信接口,换取openid和session_key。 - 后端生成自己的
token返回给前端。 - 前端把
token存到storage,后续请求头携带token。 - 后端用拦截器校验
token,未登录直接返回401。
这样做的目的是:不把微信的 session_key 暴露给前端,在答辩时讲过安全问题,老师会点头。
2.2 核心业务表设计
我不建议按网上很多模板那样拆几十张表,核心几张表能覆盖业务就行,表多了反而讲不清楚。我建议:
学生表(student)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| openid | varchar(64) | 微信唯一标识 |
| student_no | varchar(32) | 学号 |
| name | varchar(32) | 姓名 |
| class_name | varchar(64) | 班级 |
| major | varchar(64) | 专业 |
| create_time | datetime | 创建时间 |
教师表(teacher)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| openid | varchar(64) | 微信唯一标识 |
| teacher_no | varchar(32) | 工号 |
| name | varchar(32) | 姓名 |
| title | varchar(32) | 职称 |
| department | varchar(64) | 院系 |
课程表(course)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| course_name | varchar(64) | 课程名称 |
| course_code | varchar(32) | 课程编号 |
| teacher_id | bigint | 授课教师 |
| credit | decimal(3,1) | 学分 |
| total_hours | int | 总学时 |
| max_students | int | 选课容量上限 |
| selected_count | int | 当前已选人数 |
| semester | varchar(32) | 开课学期 |
| course_desc | text | 课程简介 |
| status | tinyint | 状态:0未开始 1选课中 2已结束 |
选课表(course_selection)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| student_id | bigint | 学生id |
| course_id | bigint | 课程id |
| select_time | datetime | 选课时间 |
| status | tinyint | 状态:0已退课 1已选课 |
| score | decimal(5,2) | 最终成绩 |
课程内容表(course_content)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| course_id | bigint | 所属课程 |
| chapter_title | varchar(128) | 章节标题 |
| content_type | tinyint | 1视频 2文档 3图文 |
| content_url | varchar(255) | 内容地址 |
| sort_order | int | 排序号 |
这几张表之间用外键逻辑关联(实际项目不一定建物理外键,用索引即可),course_selection就是选课模块的核心,学生选课、退课、成绩查询都在这张表上做文章。答辩时把这个ER关系讲明白,数据设计这块就稳了。
2.3 选课并发控制——这个细节能让你超出同学一大截
很多同学习惯性这么写选课逻辑:
// 不推荐:查询后判断再插入 Course course = courseMapper.selectById(courseId); if (course.getSelectedCount() >= course.getMaxStudents()) { // 提示已满 } // 插入选课记录 // 更新课程人数这个顺序在单机低并发下没问题,但假设你们学校答辩演示时有几十个学生同时抢一门课,数据一错乱就说不清了。真实场景里这叫“超选问题”。
我从实际开发的角度给你一个相对好实现的方案:在course_selection表上建唯一索引(student_id, course_id),再配合事务控制。具体逻辑:
- 开启事务。
- 先查是否已选过该课程。如果已存在记录,直接抛异常“请勿重复选课”。
- 执行更新语句,用乐观锁思想控制人数:
UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} AND selected_count < max_students- 如果
update影响行数为 0,说明课程已满,抛异常“选课人数已满”。 - 插入选课记录,提交事务。
这个方案不需要引入Redis分布式锁,对教学项目来说完全够用,且代码量不大。把这条写进你的设计文档里,答辩时说出来,老师会认为你真的思考过并发问题。
3. 后端接口开发与实现细节
3.1 接口风格与统一返回格式
接口设计我强烈建议走RESTful风格,别用那种在URL里写?action=xxx的老式写法。后台接口统一以/api/开头,然后按模块划分:
| 模块 | 接口前缀 |
|---|---|
| 学生认证 | /api/student/auth |
| 教师认证 | /api/teacher/auth |
| 课程模块 | /api/course |
| 选课模块 | /api/selection |
| 学习模块 | /api/learn |
课程模块、选课模块、学习模块是三个大方向,每个方向能写四五个接口,合起来接口总量就有二十多个,足够撑起项目的复杂度。
统一返回格式我建议封装一个Result类:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }前端小程序里统一处理code,是200就渲染数据,不是200就弹message。这样做的好处是:前后端联调时约定好一个格式,所有接口都按这个格式走,永远不会乱。
有一个很多同学容易忽略但极其重要的点:后端返回前端的时间字段要么格式化后返回,要么返回时间戳,千万不要直接返回一个Date对象扔给前端。因为Java序列化Date默认输出格式是"2025-07-01T10:30:00.000+08:00"这种,小程序端解析起来非常费劲。我一般是在application.yml里统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这样前端拿到的就是"2025-07-01 10:30:00",直接往页面上怼就行。
3.2 核心接口演示:选课接口的完整实现
我拿最典型的选课接口来示范,让你感受一下完整链路。
后端Controller层:
@RestController @RequestMapping("/api/selection") public class SelectionController { @Resource private SelectionService selectionService; @PostMapping("/select") public Result<Void> selectCourse(@RequestParam Long studentId, @RequestParam Long courseId) { selectionService.selectCourse(studentId, courseId); return Result.success(null); } }Service层才是核心,代码写这里:
@Service public class SelectionServiceImpl implements SelectionService { @Resource private CourseSelectionMapper selectionMapper; @Resource private CourseMapper courseMapper; @Override @Transactional(rollbackFor = Exception.class) public void selectCourse(Long studentId, Long courseId) { // 1. 校验学生和课程是否存在 Course course = courseMapper.selectById(courseId); if (course == null) { throw new BusinessException("课程不存在"); } // 2. 若已选过则直接报错 Long count = selectionMapper.selectCountByStudentIdAndCourseId(studentId, courseId); if (count > 0) { throw new BusinessException("您已选过该课程"); } // 3. 乐观锁更新选课人数,update影响行数为0说明课程已满 int rows = courseMapper.updateSelectedCount(courseId); if (rows == 0) { throw new BusinessException("选课人数已满"); } // 4. 插入选课记录 CourseSelection selection = new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selection.setStatus(1); selection.setSelectTime(new Date()); selectionMapper.insert(selection); } }这段代码里,updateSelectedCount对应的Mapper写法:
<update id="updateSelectedCount"> UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} AND selected_count < max_students </update>注意,这里的<是XML里小于号的转义写法。这个细节看起来小,但在实操中好多人在XML里直接写<导致解析报错,我还见过改了半天没发现问题的。你在自己动手时记得这个坑。
对应退课接口逻辑类似,但要把人数扣回去,同时判断一下成绩是否已发布——如果成绩已经录入,就不允许退课了。
3.3 课程内容学习功能怎么设计更合理
选完课之后,学生需要能查看课程内容。我见过很多实现直接把课程视频的URL放到课程表里,一个课程一个视频,简单粗暴。但这种设计答辩时容易被追问:“如果一个课程有十几个章节视频,你怎么存?”
更稳妥的做法是设计独立的course_content表(前面已经列过),一个课程关联多个章节内容,每个章节有自己的content_type和content_url。前端通过课程id去查询章节列表,再逐个章节渲染。这样设计扩展性强,以后加课时、加课件都容易。
学生在查看视频或文档时,我还建议加一个简单的“学习进度”标记,不需要做成视频播放时长统计那种复杂的逻辑,只需要做一个已学/未学标记即可。这样在课程详情页能展示“共12章,已学8章”,项目的功能深度就有了。
学习记录表设计如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| student_id | bigint | 学生id |
| content_id | bigint | 章节内容id |
| complete_flag | tinyint | 是否完成:0未学 1已学 |
| update_time | datetime | 最后学习时间 |
通过学生id去查学习记录,算出完整度。这个过程也适合用来做成绩评估的参考字段。
4. 微信小程序端页面开发要点
4.1 页面结构与常用组件
小程序的页面建议按下面这个结构分:
| 页面路径 | 功能 |
|---|---|
| pages/login | 微信授权登录 |
| pages/home | 首页/课程列表 |
| pages/course-detail | 课程详情与选课 |
| pages/my-courses | 我的课程/学习入口 |
| pages/learn | 课程内容学习页 |
| pages/mine | 个人中心 |
| pages/teacher-course | 教师端课程管理入口 |
页面不用做太多,7个左右足够,但你得把每个页面的业务逻辑做清楚。比如首页不能只是展示几张图,要能下拉刷新、分页加载课程列表、按学期筛选。
小程序端公共代码我建议做三块基础封装:
- request.js:统一请求方法,自动附带token,拦截状态码,避免每个页面重复写
wx.request。 - config.js:保存后端接口地址前缀,方便切换测试环境和生产环境。
- utils.js:格式化时间、校验工具等公共函数。
request.js的核心逻辑大概长这样:
const BASE_URL = 'http://localhost:8080/api'; function request(url, method, data) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: BASE_URL + url, method: method || 'GET', data: data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success(res) { if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); if (res.data.code === 401) { // 未登录跳转到登录页 wx.reLaunch({ url: '/pages/login/login' }); } reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };这个封装在答辩时讲“前端工程化”会非常加分。老师看到你不仅写了页面,还把公共request、数据缓存、状态码统一处理都做了,这就不是照着教程敲代码的水平了。
4.2 条件渲染与列表展示的坑
课程列表页用wx:for渲染。有一个常见的问题是:课程封面图如果为空,image组件会有一个默认的灰底,看起来很不美观。处理办法是提前给一个默认图:
<image src="{{item.coverUrl || '/assets/default-course.png'}}" mode="aspectFill"></image>选课按钮的状态切换也要注意。当status是“选课中”时展示红色“选课”,当已选且有名额时展示灰色“已选”,当课程已满时展示“已满”,这三种状态看起来简单,但如果后端不下发一个明确的状态字段,前端就只能靠胡乱拼逻辑判断,导致一团糟。我建议后端接口直接返回一个可枚举的selectionStatus,而不是让前端自己去比较时间戳和人数。
4.3 后端部署需要把域名配好
开发时可以使用http://localhost:8080,但我建议换成本机IP(比如http://192.168.1.100:8080),然后在小程序开发者工具的“详情-本地设置”里勾选“不校验合法域名”。这两个方式联调没问题。
但一旦要发布体验版,就得有https域名,多为证书和服务器操心是值得的。我在实际操作中的经验是:直接买一台云服务器然后部署后端,小程序端请求域名的证书用免费的就行,这样演示环境也方便。
如果你想让项目直接运行视频里用本地环境,一套localhost:8080就能跑,别人拿到项目后不需要再买域名和服务器,这对很多没经验的同学来说非常友好。
5. 常见问题与排查技巧实录
5.1 学生端拿不到课程列表?查这个
现象:小程序页面一直转圈,控制台报错request:fail。
排查思路:
- 先确认后端服务有没有启动。直接在浏览器访问
http://localhost:8080/api/course/list,看能不能拿到JSON数据。 - 如果浏览器能访问而小程序不行,基本就是域名校验问题——去开发者工具里勾选“不校验合法域名”。
- 如果浏览器也访问不了,看后端报错日志。常见原因是数据库连接配错,
application.yml里数据库账号密码跟本地MySQL不一致。
提示:改完
application.yml记得重启SpringBoot应用,这个问题很多同学第一天开发都会踩。
5.2 选课提示“人数已满”但明明有空位
现象:课程列表显示选了2人/100人,但一点选课就提示已满。
排查思路:
这个问题的九成原因都是select_count和max_students字段类型或初始值配错了。比如max_students默认是0但没设置成100,SQL里selected_count < max_students就永远等于0 < 0不成立,于是所有课程都是满的。
还有一种是updateSelectedCount里条件写反了:
-- 错误写法(负负得正,但逻辑完全相反) UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} AND selected_count > max_students这个条件写成大于号,永远影响0行,代码就报“已满”。检查代码时不要只盯Service,Mapper的XML也要一个字符一个字符过。
5.3 token失效后小程序白屏
现象:用户登录后隔一段时间再打开,页面一直加载失败。
原因:token失效了,后端返回401,但前端没有做路由跳转处理。
解决:在request.js里统一判断状态码,401时清理本地缓存,然后重定向到登录页。这个是全局拦截的意义所在,不要只在某个页面处理。
5.4 视频课程加载不出来
现象:学习页面的视频黑屏或提示“无效的播放地址”。
排查思路:
- 如果视频是从本地上传的,检查一下文件路径是不是写成了
C:\Users\xxx\...这样的本地绝对路径。后端要返回可访问的HTTP路径,比如http://192.168.1.100:8080/static/videos/xxx.mp4。 - 如果是外链视频,确认是不是微信小程序允许的视频格式。微信小程序的
video组件支持mp4、m3u8等格式,但有些自定义的无后缀流地址不支持。 - 跨域问题:后端要配置允许跨域,否则小程序请求视频资源时会失败。
跨域配置我一般这样写:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }5.5 教师端上传课件失败
现象:教师发布课程时上传课件一直失败。
排查思路:
大多数情况下是spring.servlet.multipart.max-file-size默认才1MB,课件PPT几MB上传必然报错。我在项目里一般配置成:
spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB另外,文件上传的保存路径不要写死成绝对路径,建议放到一个可配置的目录,比如:
@Value("${file.upload-dir}") private String uploadDir;然后在application.yml里配置:
file: upload-dir: ./uploads这样项目拿到什么电脑上都能跑,不会因为路径不存在而上传失败。
6. 打包、部署与交付的那些事
6.1 后端怎么打包运行
这是我收到过最多的问题之一,但很多教程反而没写清楚。后端用的是SpringBoot内置Tomcat,不需要额外装Tomcat,也不需要打WAR包,直接打JAR包:
cd 项目根目录 mvn clean package -DskipTests打包完成后在target/目录下能看到xxx.jar,启动命令:
java -jar target/xxx.jar建议在application.yml里配置好生产环境的数据库连接和端口:
server: port: 8080部署到服务器上可以配合nohup命令在后台运行:
nohup java -jar xxx.jar --spring.profiles.active=prod > app.log 2>&1 &日志输出到app.log,排查问题直接看这个文件。
6.2 运行视频和讲解视频的价值
这个标题里带了“源码+文档+运行视频+讲解视频”。不少同学可能会忽略这个配套的价值,我这几年看的经验是:买源码的人一半以上跑不起来,跑起来的也讲不清楚。
运行视频的意义在于:你拿到项目后照着视频就能把环境搭起来,省去自己踩环境变量的坑。讲解视频的意义更实际——答辩讲稿可以直接从视频里提炼,老师问到的每个模块,视频里都讲到过。
所以我建议你拿到项目后,第一遍先看运行视频,把项目跑通;第二遍再看讲解视频,理清项目的架构和核心代码位置。一定不要上来就埋头读源码,没有全局视角的源码阅读效率极低,三天下来跟没看一样。
6.3 文档要怎么写才能拿高分
配套文档一般包括开题报告、任务书、中期检查表、毕业论文。毕业论文是关键,我建议逻辑主线这样安排:
- 第一章:背景与意义。写校园选课管理的痛点,不要写太空泛的“随着科技的发展”,直接写“传统线下选课方式存在的排队耗时、数据易错、信息不透明等问题”。
- 第二章:技术介绍。SpringBoot的自动配置原理、微信小程序的运行架构、MySQL事务特性,每个技术点写两页左右即可。
- 第三章:需求分析。画用例图、分析三类角色各自的需求。
- 第四章:系统设计。架构图、功能模块图、数据库ER图。
- 第五章:系统实现。按模块贴核心代码,不要贴大段完整代码——老师要的是讲解,不是代码全文。
另外,文档里不要忘记画三张图:系统架构图、功能模块图、数据库ER图。三张图纸一步到位的项目,在评阅老师那里印象分会直线上涨。
这行我只强调一个细节:学位论文的查重一般看连续13个字重复,所以凡是引用的概念定义都要自己重新组织语言,不能直接照抄百度百科。源代码部分一般来说不参与查重,但如果你逐字粘贴网上的全篇项目源码,还是有一定风险。最好把核心代码用文字讲清楚,代码片段只贴关键逻辑。
7. 实际开发中的经验与进阶建议
7.1 我做完这个项目之后,回头看最有用的几个习惯
不要急着写代码,先把ER图和数据字典定下来。这是我反复吃亏换来的教训。有一次我把课程表设计得过于简单,结果做到学习模块时发现没有章节表,只能中途补表,导致选课、学习、成绩相关的接口全部跟着改,浪费了大半个周末。
另一个习惯是:前端每个页面的接口请求,都先在开发者工具中手测一遍。小程序开发者工具的合法性校验可以先关掉,但正式演示前一定要用真机预览测一遍,因为真机和模拟器的网络环境不一样,代理设置也不一样。很多项目模拟器上好好的,一点真机预览就全挂了。
7.2 后续可以怎么扩展
做完这个基础版本后,如果想拿它找工作或者继续深耕,扩展方向很明确:
- 引入Redis做缓存和分布式锁:课程列表数据缓存到Redis,选课用Redis+Lua脚本控制并发。
- 引入消息队列:真正的高峰选课用消息队列削峰填谷,这个作为论文的“系统优化”章节素材很有说服力。
- 引入权限框架:把现在自己写的拦截器升级成Spring Security或Sa-Token,更贴近企业开发。
- 增加在线考试模块:填空题、选择题自动判分,这个功能一做出来,系统复杂度明显提升。
- 增加通知模块:订阅消息(课程开课提醒、成绩发布提醒),小程序官方能力,用起来门槛不高。
如果你时间充裕,我强烈建议加一个“在线考试”功能。因为目前的项目偏向“选课+内容浏览”,考试功能可以引入更复杂的业务逻辑:试卷生成、答题提交、自动判分、成绩统计。有了这块,答辩时根本不怕老师追问,越问你有越多东西可以讲。
7.3 最后再分享一个真实踩坑经验
团体项目我把后端端口设为8080,前端请求localhost:8080/api,一切正常。结果部署到演示环境,服务器防火墙把8080端口没开放,前端在真机上怎么都连不上。排查了大半天,最后发现就是防火墙的问题。在服务器上运行:
firewall-cmd --zone=public --add-port=8080/tcp --permanent firewall-cmd --reload顺便把MySQL的3306端口要不要对外开放想清楚——如果只想让后端连数据库,3306就别对公网开,减少安全隐患。我在实际部署中吃过亏,3306端口暴露在公网后收到过不少恶意扫描告警。
说白了,做这类校园项目,真正的难点不在于某个单独知识点有多难,而是要把前端小程序、后端接口、数据库、部署环境这几层全部串起来。每层单独看都不复杂,层与层之间的关联才是磨人的地方。但这个项目帮你把链路走通一遍之后,以后再遇到类似的软件开发需求,你心里就有底了。