SpringBoot+微信小程序智慧校园选课系统开发实战
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。后端用codeappIdsecret请求微信接口换取openid和session_key。后端生成自己的token返回给前端。前端把token存到storage后续请求头携带token。后端用拦截器校验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学生idcourse_idbigint课程idselect_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)再配合事务控制。具体逻辑开启事务。先查是否已选过该课程。如果已存在记录直接抛异常“请勿重复选课”。执行更新语句用乐观锁思想控制人数UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count max_students如果update影响行数为 0说明课程已满抛异常“选课人数已满”。插入选课记录提交事务。这个方案不需要引入Redis分布式锁对教学项目来说完全够用且代码量不大。把这条写进你的设计文档里答辩时说出来老师会认为你真的思考过并发问题。3. 后端接口开发与实现细节3.1 接口风格与统一返回格式接口设计我强烈建议走RESTful风格别用那种在URL里写?actionxxx的老式写法。后台接口统一以/api/开头然后按模块划分模块接口前缀学生认证/api/student/auth教师认证/api/teacher/auth课程模块/api/course选课模块/api/selection学习模块/api/learn课程模块、选课模块、学习模块是三个大方向每个方向能写四五个接口合起来接口总量就有二十多个足够撑起项目的复杂度。统一返回格式我建议封装一个Result类public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }前端小程序里统一处理code是200就渲染数据不是200就弹message。这样做的好处是前后端联调时约定好一个格式所有接口都按这个格式走永远不会乱。有一个很多同学容易忽略但极其重要的点后端返回前端的时间字段要么格式化后返回要么返回时间戳千万不要直接返回一个Date对象扔给前端。因为Java序列化Date默认输出格式是2025-07-01T10:30:00.00008:00这种小程序端解析起来非常费劲。我一般是在application.yml里统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样前端拿到的就是2025-07-01 10:30:00直接往页面上怼就行。3.2 核心接口演示选课接口的完整实现我拿最典型的选课接口来示范让你感受一下完整链路。后端Controller层RestController RequestMapping(/api/selection) public class SelectionController { Resource private SelectionService selectionService; PostMapping(/select) public ResultVoid 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 idupdateSelectedCount 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学生idcontent_idbigint章节内容idcomplete_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个左右足够但你得把每个页面的业务逻辑做清楚。比如首页不能只是展示几张图要能下拉刷新、分页加载课程列表、按学期筛选。小程序端公共代码我建议做三块基础封装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}} modeaspectFill/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但没设置成100SQL里selected_count max_students就永远等于0 0不成立于是所有课程都是满的。还有一种是updateSelectedCount里条件写反了-- 错误写法负负得正但逻辑完全相反 UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count max_students这个条件写成大于号永远影响0行代码就报“已满”。检查代码时不要只盯ServiceMapper的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.activeprod app.log 21 日志输出到app.log排查问题直接看这个文件。6.2 运行视频和讲解视频的价值这个标题里带了“源码文档运行视频讲解视频”。不少同学可能会忽略这个配套的价值我这几年看的经验是买源码的人一半以上跑不起来跑起来的也讲不清楚。运行视频的意义在于你拿到项目后照着视频就能把环境搭起来省去自己踩环境变量的坑。讲解视频的意义更实际——答辩讲稿可以直接从视频里提炼老师问到的每个模块视频里都讲到过。所以我建议你拿到项目后第一遍先看运行视频把项目跑通第二遍再看讲解视频理清项目的架构和核心代码位置。一定不要上来就埋头读源码没有全局视角的源码阅读效率极低三天下来跟没看一样。6.3 文档要怎么写才能拿高分配套文档一般包括开题报告、任务书、中期检查表、毕业论文。毕业论文是关键我建议逻辑主线这样安排第一章背景与意义。写校园选课管理的痛点不要写太空泛的“随着科技的发展”直接写“传统线下选课方式存在的排队耗时、数据易错、信息不透明等问题”。第二章技术介绍。SpringBoot的自动配置原理、微信小程序的运行架构、MySQL事务特性每个技术点写两页左右即可。第三章需求分析。画用例图、分析三类角色各自的需求。第四章系统设计。架构图、功能模块图、数据库ER图。第五章系统实现。按模块贴核心代码不要贴大段完整代码——老师要的是讲解不是代码全文。另外文档里不要忘记画三张图系统架构图、功能模块图、数据库ER图。三张图纸一步到位的项目在评阅老师那里印象分会直线上涨。这行我只强调一个细节学位论文的查重一般看连续13个字重复所以凡是引用的概念定义都要自己重新组织语言不能直接照抄百度百科。源代码部分一般来说不参与查重但如果你逐字粘贴网上的全篇项目源码还是有一定风险。最好把核心代码用文字讲清楚代码片段只贴关键逻辑。7. 实际开发中的经验与进阶建议7.1 我做完这个项目之后回头看最有用的几个习惯不要急着写代码先把ER图和数据字典定下来。这是我反复吃亏换来的教训。有一次我把课程表设计得过于简单结果做到学习模块时发现没有章节表只能中途补表导致选课、学习、成绩相关的接口全部跟着改浪费了大半个周末。另一个习惯是前端每个页面的接口请求都先在开发者工具中手测一遍。小程序开发者工具的合法性校验可以先关掉但正式演示前一定要用真机预览测一遍因为真机和模拟器的网络环境不一样代理设置也不一样。很多项目模拟器上好好的一点真机预览就全挂了。7.2 后续可以怎么扩展做完这个基础版本后如果想拿它找工作或者继续深耕扩展方向很明确引入Redis做缓存和分布式锁课程列表数据缓存到Redis选课用RedisLua脚本控制并发。引入消息队列真正的高峰选课用消息队列削峰填谷这个作为论文的“系统优化”章节素材很有说服力。引入权限框架把现在自己写的拦截器升级成Spring Security或Sa-Token更贴近企业开发。增加在线考试模块填空题、选择题自动判分这个功能一做出来系统复杂度明显提升。增加通知模块订阅消息课程开课提醒、成绩发布提醒小程序官方能力用起来门槛不高。如果你时间充裕我强烈建议加一个“在线考试”功能。因为目前的项目偏向“选课内容浏览”考试功能可以引入更复杂的业务逻辑试卷生成、答题提交、自动判分、成绩统计。有了这块答辩时根本不怕老师追问越问你有越多东西可以讲。7.3 最后再分享一个真实踩坑经验团体项目我把后端端口设为8080前端请求localhost:8080/api一切正常。结果部署到演示环境服务器防火墙把8080端口没开放前端在真机上怎么都连不上。排查了大半天最后发现就是防火墙的问题。在服务器上运行firewall-cmd --zonepublic --add-port8080/tcp --permanent firewall-cmd --reload顺便把MySQL的3306端口要不要对外开放想清楚——如果只想让后端连数据库3306就别对公网开减少安全隐患。我在实际部署中吃过亏3306端口暴露在公网后收到过不少恶意扫描告警。说白了做这类校园项目真正的难点不在于某个单独知识点有多难而是要把前端小程序、后端接口、数据库、部署环境这几层全部串起来。每层单独看都不复杂层与层之间的关联才是磨人的地方。但这个项目帮你把链路走通一遍之后以后再遇到类似的软件开发需求你心里就有底了。