
这份选题是Java Web方向里真正能打的“常青树”项目——高校学生选课系统。一个人口不到两千的学校每学期选课几万条记录涉及学生、教师、管理员三类角色包含选课、退课、排课、成绩录入全流程业务边界清晰难度又刚好卡在本科毕设的黄金区间既有CRUD以外的并发逻辑又不至于让你陷入分布式、高可用的泥潭。如果你正在为毕设选题发愁或者已经拿到了这套源码但不知道怎么跑起来、怎么改造成自己的项目这篇文章会把整个系统的骨架、数据库设计、接口逻辑、环境坑点拆开讲透。我尽量用“带过毕设的师兄”的视角把文档里不会写的东西也补上。1. 项目整体拆解高校选课系统的核心业务闭环很多人拿到源码第一件事就是启动、点点点、截图这其实是最亏的打开方式。选课系统这种经典题目真正的学习价值在于理解它的业务闭环是怎么通过代码落地的。你答辩的时候老师最常问的一句话就是“你这个系统的核心业务流程是什么遇到并发怎么处理”这两个问题恰好就是这套系统的命门。1.1 三个角色的权限边界高校选课系统的用户模型非常经典几乎可以作为所有“角色权限”类项目的模板。系统里一共有三种身份学生浏览课程、选课、退课、查看课表、查看成绩、个人信息维护教师查看自己开设的课程、录入修改成绩、查看选课学生名单管理员学生/教师账号管理、课程信息维护、选课时间窗口配置、系统数据统计权限设计的核心是后端接口校验而不是前端隐藏按钮。前端你随便用Vue改个菜单就能把管理员的入口露出来真正的防线在SpringBoot的拦截器里。这套项目通常会用拦截器读取请求头里的token解析出用户角色再比对当前访问接口所需的权限。比SysUserRole表这类毕设规模完全没有必要一个role字段就够了三种角色值student、teacher、admin。1.2 选课业务的核心冲突时间窗口与容量控制选课系统最关键的业务规则有两条第一选课有时间窗口。管理员设置一个选课开放时间和截止时间这个配置通常放在数据库一张配置表里学生在窗口外不允许操作。为什么这个功能必须做因为没有时间窗口控制学生可以随便选课甚至半夜改课表整个学期的教学安排就乱套了。第二课程有容量限制。一门课可能只收80人选满了就不能再选。这个看起来简单的规则恰恰是并发问题的根源。假如有100个学生同时抢一门只剩5个名额的课如果代码只是查一下剩余名额、判断大于0就插入选课记录那5个名额瞬间可以冒出8条甚至更多记录。1.3 为什么这个题目是Java Web毕设的常青树我自己带过几届毕业设计老实说大部分题目都“活不过答辩”。旅游类项目没特色图书管理类太单调商城类又容易陷入支付方案的泥潭。选课系统之所以经久不衰是因为它有三个特质业务天然闭环从创建课程 → 学生选课 → 教师录成绩 → 学生查成绩每个动作在真实校园里都对应一个场景难度刚好卡位增删改查能跑通够毕业加上事务、唯一约束、乐观锁就能把“并发”这个亮点讲出深度改造成本低换成实验室预约、场馆预订、在线考试报名只需要替换实体字段表结构几乎不用动比如你把这个系统改造成“高校实验室预约系统”核心流程完全一致管理员开放预约 → 学生在时间段内预约 → 实验室管理员审核 → 学生查看预约记录。甚至前端页面都能复用大半。2. 技术选型背后SpringBoot Vue.js前后端分离的取舍题目已经把所有技术栈写明了SpringBoot后端 Vue.js前端 MySQL SQL脚本 接口文档。这套组合在2024年几乎就是Java Web毕设的默认配置了但你要能说清楚“为什么选它”而不是“别人都这么选”答辩才能加分。2.1 后端为什么是SpringBoot而不是SSH或SSM很多学校教材还在讲SSHStruts2 Spring Hibernate或者SSMSpring SpringMVC MyBatis但实际开发里这些东西已经被淘汰得差不多了。SpringBoot的本质是“Spring的自动配置封装”它把原来Spring项目里几十行XML配置压缩成了几个注解。具体到选课系统SpringBoot带来的好处是实实在在的内嵌Tomcat不用再往Tomcat里拷贝war包mvn spring-boot:run或者直接启动main方法就能跑这对没配置过服务器的学生来说友好太多application.yml集中配置数据源、JWT密钥、MyBatis映射、端口号全在一个文件里清晰明了起步依赖引入什么场景就加什么starter比如spring-boot-starter-web、mybatis-plus-boot-starter不会像SSM那样手动拷一堆jar包还冲突这套项目如果用纯SSM写光配置文件就得写200多行而且每个配置文件之间的引用关系非常隐蔽一旦报错新手基本懵圈。SpringBoot把这一层复杂度吃掉了所以你能把精力花在业务逻辑上——也就是答辩时真正值钱的地方。2.2 前端为什么是Vue.js Element UIVue.js在这个项目里的角色是“渐进式前端框架”。它不是那种刷新页面跳转的传统网页JSP而是整个页面由一个Vue实例接管后端只提供JSON数据点击“选课”按钮前端发一个Ajax请求局部更新页面状态。Element UI是Vue.js专用的组件库相当于给前端提供了一套已经做好样式的控件表格、表单、分页、日期选择器、弹窗、消息提示。你自己手写一套这样的UI组件少说也得2000行CSS加JavaScript用Element UI就是引用组件然后绑数据的事。前后端分离和传统JSP开发最本质的区别可以用一个例子说明传统方式里页面是后端用Java拼出来的字符串分离模式下前端页面是静态文件后端只返回{ code: 200, data: [...] }这种JSON。两边通过网络通信前端跑在8080端口后端跑在9090端口开发时前端加个代理转发请求避免跨域问题生产环境则用Nginx把两者合成一个域名。2.3 接口文档的价值拿着就能联调这套项目附带接口文档这一点很多学生不重视但它恰恰是项目“完整度”最直接的体现。接口文档解决的问题是前端和后端怎么约定数据格式。没有接口文档你前后端联调时反复问“你这个接口返回的字段是count还是totalCount”、“日期传字符串还是时间戳”一个项目拖一两周。有了文档前端直接照着写axios请求后端照着返回两边不用等对方写代码。最常见的接口格式是{ code: 200, message: 操作成功, data: { courseId: 1, courseName: Java程序设计, teacherName: 张老师, credit: 3, selected: 45, capacity: 80 } }code用于标识业务成功与否data是真正的业务数据message用于前端弹窗提示。这样一套统一格式写10个接口和写100个接口模式都是一样的。3. 数据库设计与SQL脚本核心表结构与初始化数据拿到SQL脚本第一件事千万不要直接执行完就跑。我见过太多学生执行source course.sql之后连show tables都没敲过就开始启动后端结果数据库连接失败都不知道问题在哪。先看建表语句读懂每张表的作用后续改需求心里才有底。3.1 从E-R图到建表语句五张表怎么撑起一套系统一个标准的选课系统最少只需要6张表表名作用核心字段user统一账号表存所有登录身份id, username, password, rolestudent学生详细信息user_id, name, student_no, class_nameteacher教师详细信息user_id, name, teacher_no, titlecourse课程基本信息id, course_name, credit, capacity, selected_count, teacher_idcourse_record选课记录表也叫选课明细id, student_id, course_id, status, select_timeconfig系统配置表存选课开放时间等config_key, config_value学生跟用户是一对一关系student.user_id关联user.id。选课记录是学生和课之间的“关联表”这是最核心的设计。为什么选课记录不能被合并到课程表里因为一门课会关联很多学生、一个学生会选择很多门课这就是典型的多对多关系必须拆出一张中间表。课程表里的selected_count字段很关键它代表当前已经选了多少人每次成功选课就1退课就-1。这是典型的空间换时间的冗余设计如果每次要看剩余名额都要去course_record里count(*)课程列表页一次展示10条数据就要跑10次查询性能很差。3.2 改造成自己毕设时SQL脚本需要动哪里拿到的SQL脚本一定带有原始项目的痕迹比如表名可能叫t_course、字段里可能有c_id这种缩写风格。改造时我建议统一整理一遍把表名改成语义化的英文比如course_record而不是sc或elective每个表都加上create_time和update_time字段哪怕逻辑上暂时用不到答辩时解释“冗余设计方便后续维护”也能加分统一主键为id自增避免UUID主键引发连表查询性能问题删除你不用的示例数据比如原始项目里“张三、李四”这种测试学生账号要清干净换成你自己设计的用户信息还有一个很多学生会忽略的细节字符集。建表语句里指定ENGINEInnoDB DEFAULT CHARSETutf8mb4如果你图省事用默认的latin1你插入中文字段就会变成一堆问号。我在实战中看到至少三分之一的学生栽在这个问题上。3.3 初始化数据必须手工造SQL脚本里的INSERT INTO语句一定不能全盘信任因为原始项目的数据可能跟你的业务对不上。但种子数据又必须有否则系统登录进去空空如也截图都不好看。我是这么处理的管理员账号admin / admin123密码用MD5加密后写入数据库注意SpringBoot项目里多半用了加盐加密需要看下PasswordEncoder的实现学生账号造10个账号规则用学号例如2021010101 / 123456教师账号造4个分布在不同的二级学院 课程数据一个学期12门课覆盖周一至周五、每天1-8节容量从30到120不等初始化数据时有个小技巧把最热门的那门课容量设置为80但selected_count预先填入78这样你演示选课的时候滑块或者前端进度条会有“抢最后一两个名额”的紧张感演示效果比选择一架空空荡荡的课程好得多。4. 接口文档与核心API实现登录、选课、退课背后的事接口文档不是摆设它是你做二次开发的地图。选课系统里最核心的几个接口每一个都有值得展开讲的细节。作为毕设你不需要把每个接口都背下来但接口设计的思想和核心逻辑能讲清楚就足够优秀了。4.1 登录鉴权Shiro还是JWT这个项目怎么选传统Java Web项目经常用Session保持登录状态前后端分离项目则普遍使用JWTJSON Web Token。流程是这样的用户提交账号密码后端校验通过生成一段带签名信息的Token字符串前端拿到Token存在localStorage里之后每次请求都在Header里带上Authorization: Bearer token后端过滤器拦截请求解析Token把用户身份还原出来放到上下文里JWT的好处是服务端不用存Session天然适合前后端分离。毕设项目里你只需要一个拦截器加一个工具类就能实现代码量很小。比较常见的实现方式是用io.jsonwebtoken:jjwt这个库生成Token的代码就几行public String generateToken(User user) { return Jwts.builder() .claim(userId, user.getId()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 86400000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }Token有效期设置成24小时比较合理。太短学生没上一次课就掉线太长又有安全隐患。4.2 选课和退课的核心逻辑这段代码是答辩重点选课接口是整个系统最核心的“亮点代码”。基础版逻辑很多人能写出来但带上事务和并发控制的版本才是真正值钱的Transactional(rollbackFor Exception.class) public Result selectCourse(Long studentId, Long courseId) { // 1. 判断是否在选课时间窗口内从config表读取 LocalDateTime now LocalDateTime.now(); if (now.isBefore(selectStartTime) || now.isAfter(selectEndTime)) { return Result.error(当前不在选课时间内); } // 2. 判断是否已经选过这门课防止重复选课 Integer count courseRecordMapper.countByStudentAndCourse(studentId, courseId); if (count 0) { return Result.error(请不要重复选课); } // 3. 使用乐观锁更新课程容量where条件里带上selected_count int rows courseMapper.decreaseCapacity(courseId); if (rows 0) { return Result.error(课程已经选满); } // 4. 插入选课记录 CourseRecord record new CourseRecord(); record.setStudentId(studentId); record.setCourseId(courseId); record.setStatus(1); courseRecordMapper.insert(record); return Result.success(选课成功); }这段代码最精妙的地方在第3步。decreaseCapacity对应的SQL是UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count capacity这里的AND selected_count capacity就是乐观锁。它保证数据库在更新的时候再次校验容量即使100个并发请求同时到达数据库的行级锁也会让它们排队执行只有前80个能把selected_count从79更新到80后面的更新因为条件不成立而影响行数为0。Transactional保证第3步和第4步要么一起成功要么一起失败。如果插入选课记录失败容量更新会一并回滚不会出现“课没选上但名额被占了”的情况。4.3 接口文档里的字段命名有坑接口文档你仔细看会发现有些字段名不是Java风格的驼峰命名而是下划线风格比如student_id、course_name。这是MyBatis或者MySQL字段自动映射的结果。如果后端返回JSON给前端建议用JsonProperty或者配置MyBatis的驼峰自动匹配mybatis-plus: configuration: map-underscore-to-camel-case: true这么配置之后数据库里的course_id字段会自动映射到Java实体的courseId上JSON返回给前端也自然变成了courseId。如果不配这个你会在前端看到course_id这种怪字段运气好能跑通但在明眼人看来这就是基本功不扎实。5. 项目运行与环境踩坑实录实话实说折腾环境这一步比写代码还劝退人。我见过太多学生卡在“项目跑不起来”最后找我救火其实大部分问题到了就那几个固定的坑。我把从零启动这套项目的完整过程和常见坑一次性整理出来照着做能省很多时间。5.1 三步把前后端项目跑起来第一步准备数据库环境先确认本机MySQL版本是5.7以上创建数据库CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4; USE course_system; SOURCE course_system.sql;注意执行SQL脚本之前检查脚本里有没有DROP TABLE IF EXISTS语句。如果脚本没有而你之前已经执行过一次再执行会报表已存在手动DROP即可。第二步启动SpringBoot后端用IDE打开后端项目目录等待Maven把依赖下载完。修改application.yml里的数据源配置spring: datasource: url: jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码这里最大的坑是serverTimezone不匹配导致的时间报错加上Asia/Shanghai基本稳妥。然后直接运行CourseSystemApplication的main方法看到日志输出“Tomcat started on port(s): 9090”就代表后端启动成功。第三步启动Vue前端进入前端目录先安装依赖npm install npm run serve如果npm install卡住或者报错看看是不是npm源的问题换成国内镜像源npm config set registry https://registry.npmmirror.com启动成功后终端会给一个本地地址通常是http://localhost:8081浏览器打开用admin/admin123登录即可。5.2 版本匹配问题一个表格说清楚这套项目因为年代变化依赖版本可能有差异最常见的版本不兼容问题列在下面组件推荐版本坑点说明JDK8 或 11JWT库和高版本JDK不兼容会报NoSuchMethodErrorMaven3.63.8对镜像源有默认限制需要手动配国内仓库Node.js14 或 16Vue CLI 4.x在Node 18以上会报OpenSSL错误MySQL5.7 或 8.0驱动版本跟数据库版本要配套MyBatis Plus3.4.x3.5.x改了部分API旧代码可能报类型转换错误Node 18跑旧版Vue项目报错是重灾区报错信息通常是error:0308010C:digital envelope routines::unsupported。解决办法是更改启动命令set NODE_OPTIONS--openssl-legacy-provider npm run serve5.3 常见问题排查速查表前端登录不上提示跨域错误前后端分离开发时跨域问题几乎人人遇到。检查后端有没有跨域配置类或者单独加一个Configuration类实现WebMvcConfigurer的addCorsMappings方法。开发阶段最简单的方式是用Vue的代理// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } }前端请求路径写成/api/login代理到后端http://localhost:9090/login浏览器角度看是同一个域不存在跨域问题。启动报错Access denied for user八成是数据库账号密码不对或者远程MySQL没开权限。检查application.yml里的password字段确认和本地MySQL一致。如果是8.0版本MySQL还要确认驱动版本对应com.mysql:mysql-connector-j:8.0.33选课成功后刷新课程列表人数没变化上面的SQL逻辑里已经提到这一点常见原因是前端列表接口查的是课程表里的一个“残留字段”——比如你数据库里既有selected_count字段又通过连表查询course_record去统计人数两边的数据对不上。统一用selected_count这个冗余字段来展示即可。5.4 答辩现场最容易翻车的地方提前防一手根据我的经验答辩演示时出最多问题的是演示数据和网络环境。演示数据的问题在于你可能在原系统里选满了所有课程演示“选课”时已经无课可选。建议在答辩前一天重置数据库并且准备一个专门的演示账号只让它选了一门课剩下的课程全部留空。网络环境的问题在于答辩现场可能没网。提前把所有依赖包下载好离线情况下Maven仓库和node_modules都在项目就能本地启动。同时建议数据库用本地MySQL而不是教室里的远程数据库否则服务器一挂全场静默。最后再分享一个小技巧整套项目跑通之后改造成自己的毕设时最讨巧的做法不是从零重写而是在现有代码上做业务替换。找一张业务表把字段换成本身课题的实体比如“课程表”换成“赛事报名表”把“选课”语义替换成“报名”然后改前端菜单文本、按钮文案和部分表单字段。你会发现主体逻辑全部复用。我自己带学生实操的经验是先花一天把项目完整跑通再花两天读清楚核心表结构和选课接口第三周开始动刀改造。绝大多数人一个礼拜就能把这套项目变成属于自己的系统剩下时间全部用来打磨PPT和准备答辩问题。这比关在宿舍里从空项目起手写三个月要高效得多。