☰
SpringBoot+微信小程序智慧校园选课系统开发实战
2026/10/22 4:30:37 网站建设 项目流程

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裸奔,这只是一个演示项目的基本做法。更好的做法是:

  1. 小程序端wx.login()拿到code。
  2. 后端用code+appId+secret请求微信接口,换取openid和session_key。
  3. 后端生成自己的token返回给前端。
  4. 前端把token存到storage,后续请求头携带token。
  5. 后端用拦截器校验token,未登录直接返回401。

这样做的目的是:不把微信的 session_key 暴露给前端,在答辩时讲过安全问题,老师会点头。

2.2 核心业务表设计

我不建议按网上很多模板那样拆几十张表,核心几张表能覆盖业务就行,表多了反而讲不清楚。我建议:

学生表(student)

字段类型说明
idbigint主键
openidvarchar(64)微信唯一标识
student_novarchar(32)学号
namevarchar(32)姓名
class_namevarchar(64)班级
majorvarchar(64)专业
create_timedatetime创建时间

教师表(teacher)

字段类型说明
idbigint主键
openidvarchar(64)微信唯一标识
teacher_novarchar(32)工号
namevarchar(32)姓名
titlevarchar(32)职称
departmentvarchar(64)院系

课程表(course)

字段类型说明
idbigint主键
course_namevarchar(64)课程名称
course_codevarchar(32)课程编号
teacher_idbigint授课教师
creditdecimal(3,1)学分
total_hoursint总学时
max_studentsint选课容量上限
selected_countint当前已选人数
semestervarchar(32)开课学期
course_desctext课程简介
statustinyint状态:0未开始 1选课中 2已结束

选课表(course_selection)

字段类型说明
idbigint主键
student_idbigint学生id
course_idbigint课程id
select_timedatetime选课时间
statustinyint状态:0已退课 1已选课
scoredecimal(5,2)最终成绩

课程内容表(course_content)

字段类型说明
idbigint主键
course_idbigint所属课程
chapter_titlevarchar(128)章节标题
content_typetinyint1视频 2文档 3图文
content_urlvarchar(255)内容地址
sort_orderint排序号

这几张表之间用外键逻辑关联(实际项目不一定建物理外键,用索引即可),course_selection就是选课模块的核心,学生选课、退课、成绩查询都在这张表上做文章。答辩时把这个ER关系讲明白,数据设计这块就稳了。

2.3 选课并发控制——这个细节能让你超出同学一大截

很多同学习惯性这么写选课逻辑:

// 不推荐:查询后判断再插入 Course course = courseMapper.selectById(courseId); if (course.getSelectedCount() >= course.getMaxStudents()) { // 提示已满 } // 插入选课记录 // 更新课程人数

这个顺序在单机低并发下没问题,但假设你们学校答辩演示时有几十个学生同时抢一门课,数据一错乱就说不清了。真实场景里这叫“超选问题”。

我从实际开发的角度给你一个相对好实现的方案:在course_selection表上建唯一索引(student_id, course_id),再配合事务控制。具体逻辑:

  1. 开启事务。
  2. 先查是否已选过该课程。如果已存在记录,直接抛异常“请勿重复选课”。
  3. 执行更新语句,用乐观锁思想控制人数:
UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} AND selected_count < max_students
  1. 如果update影响行数为 0,说明课程已满,抛异常“选课人数已满”。
  2. 插入选课记录,提交事务。

这个方案不需要引入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 &lt; max_students </update>

注意,这里的&lt;是XML里小于号的转义写法。这个细节看起来小,但在实操中好多人在XML里直接写<导致解析报错,我还见过改了半天没发现问题的。你在自己动手时记得这个坑。

对应退课接口逻辑类似,但要把人数扣回去,同时判断一下成绩是否已发布——如果成绩已经录入,就不允许退课了。

3.3 课程内容学习功能怎么设计更合理

选完课之后,学生需要能查看课程内容。我见过很多实现直接把课程视频的URL放到课程表里,一个课程一个视频,简单粗暴。但这种设计答辩时容易被追问:“如果一个课程有十几个章节视频,你怎么存?”

更稳妥的做法是设计独立的course_content表(前面已经列过),一个课程关联多个章节内容,每个章节有自己的content_type和content_url。前端通过课程id去查询章节列表,再逐个章节渲染。这样设计扩展性强,以后加课时、加课件都容易。

学生在查看视频或文档时,我还建议加一个简单的“学习进度”标记,不需要做成视频播放时长统计那种复杂的逻辑,只需要做一个已学/未学标记即可。这样在课程详情页能展示“共12章,已学8章”,项目的功能深度就有了。

学习记录表设计如下:

字段类型说明
idbigint主键
student_idbigint学生id
content_idbigint章节内容id
complete_flagtinyint是否完成:0未学 1已学
update_timedatetime最后学习时间

通过学生id去查学习记录,算出完整度。这个过程也适合用来做成绩评估的参考字段。

4. 微信小程序端页面开发要点

4.1 页面结构与常用组件

小程序的页面建议按下面这个结构分:

页面路径功能
pages/login微信授权登录
pages/home首页/课程列表
pages/course-detail课程详情与选课
pages/my-courses我的课程/学习入口
pages/learn课程内容学习页
pages/mine个人中心
pages/teacher-course教师端课程管理入口

页面不用做太多,7个左右足够,但你得把每个页面的业务逻辑做清楚。比如首页不能只是展示几张图,要能下拉刷新、分页加载课程列表、按学期筛选。

小程序端公共代码我建议做三块基础封装:

  1. request.js:统一请求方法,自动附带token,拦截状态码,避免每个页面重复写wx.request。
  2. config.js:保存后端接口地址前缀,方便切换测试环境和生产环境。
  3. 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。

排查思路:

  1. 先确认后端服务有没有启动。直接在浏览器访问http://localhost:8080/api/course/list,看能不能拿到JSON数据。
  2. 如果浏览器能访问而小程序不行,基本就是域名校验问题——去开发者工具里勾选“不校验合法域名”。
  3. 如果浏览器也访问不了,看后端报错日志。常见原因是数据库连接配错,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 文档要怎么写才能拿高分

配套文档一般包括开题报告、任务书、中期检查表、毕业论文。毕业论文是关键,我建议逻辑主线这样安排:

  1. 第一章:背景与意义。写校园选课管理的痛点,不要写太空泛的“随着科技的发展”,直接写“传统线下选课方式存在的排队耗时、数据易错、信息不透明等问题”。
  2. 第二章:技术介绍。SpringBoot的自动配置原理、微信小程序的运行架构、MySQL事务特性,每个技术点写两页左右即可。
  3. 第三章:需求分析。画用例图、分析三类角色各自的需求。
  4. 第四章:系统设计。架构图、功能模块图、数据库ER图。
  5. 第五章:系统实现。按模块贴核心代码,不要贴大段完整代码——老师要的是讲解,不是代码全文。

另外,文档里不要忘记画三张图:系统架构图、功能模块图、数据库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端口暴露在公网后收到过不少恶意扫描告警。

说白了,做这类校园项目,真正的难点不在于某个单独知识点有多难,而是要把前端小程序、后端接口、数据库、部署环境这几层全部串起来。每层单独看都不复杂,层与层之间的关联才是磨人的地方。但这个项目帮你把链路走通一遍之后,以后再遇到类似的软件开发需求,你心里就有底了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询