简介:这份资源是面向高校计算机相关专业学生与毕业设计写作者的完整论文文档,题目为基于SpringBoot的学生网上选课系统设计与实现,适合需要参考选题结构、撰写毕业论文或进行同类系统开发的读者使用。压缩包内共1个文件,为一份doc格式的毕业论文,约1.29MB,可直接查阅章节排版与写作范式。论文围绕选课管理的信息化需求展开,涵盖绪论与选题动因、开发环境与技术选型、系统分析、系统设计与实现、系统测试、结论与展望等完整章节。技术层面以SpringBoot为核心框架,结合Eclipse开发环境与Mysql数据库,功能模块包含用户管理、新闻公告、课程管理、选课操作与数据统计,并给出用户表、课程表、选课表等数据库表设计思路,同时涉及需求收集、业务流程分析和可行性研究等内容,可帮助读者理解从需求到落地的论文写作脉络与系统搭建方法。目前已有92人学习,适合作为毕业设计选题与论文框架的参考材料。
1. 选课系统真正的难点不在增删改查
拿到这类资源包的人,第一反应通常是数页面:登录、课程管理、排课管理、公告管理、成绩管理,一套 CRUD 下来功能表就填满了。可真跑到几十个人同时点「选课」的时候,问题全冒出来:同一门课被选了两次、学分上限形同虚设、选课时间过了还能提交。论文里那张「选课限制表」业务字段只有数量、开始时间、结束时间三个,看着最不起眼,它却是整套学生网上选课系统的规则中枢。
这份资源是一份完整毕业论文,正文覆盖了技术选型(Eclipse + MySQL + Tomcat + Vue + SpringBoot)、系统分析、8 张实体表的物理设计和功能测试章节。它适合两类人:拿它当毕设底稿的同学,需要把论文里的表格文字落成能跑的代码;以及想找一个业务规则足够明确的小项目,练 SpringBoot 事务边界、MySQL 唯一索引和前后端分离联调的人。下面按「库表 → 后端 → 前端 → 压测排错」推进,每一步都给到可直接抄的语句。
2. 从论文实体属性图还原一套能跑的 MySQL 库
2.1 论文给的是表结构说明,不是可执行 DDL
论文 4.3.2 把学生表、教师表、课程信息表、排课信息表、选课信息表、选课限制表、学生成绩表、公告信息表加一张字典表列成了表格。列名全是拼音:yonghu_name、kecheng_types、jiaoshi_delete这种。这个命名习惯在毕设里很常见,好处是和实体名一一对应,答辩时讲得清;坏处是类型全写成 String / Integer / Date 这种泛化说法,落到 MySQL 里必须自己翻译一遍。
几个必须翻译对的点:表格里的 String 对应varchar,长度按业务给(姓名 50、课程详情 2000、图片路径 255);表格里的 Date 对应datetime而不是date,因为排课表里shangke_time要精确到节次;*_delete字段论文标注「假删」,Integer 类型,约定 1 为已删除、0 为正常,所有查询都要带上这个条件,否则删过的课程还会出现在选课列表里。字典表dic_code / dic_name / code_index / super_id这套结构,实质是用一张表承载所有下拉选项,kecheng_types、xueqi_types、jieke_types全部指过去。
表 2-1 先理清各表职责,避免后面写 SQL 时把paike和kecheng的字段记混:
| 表名 | 论文中的实体 | 关键业务字段 | 主要写入方 |
|---|---|---|---|
| yonghu | 学生 | yonghu_uuid_number 学号、yonghu_name | 管理员 |
| jiaoshi | 教师 | jiaoshi_uuid_number 工号、banji_types 班级 | 管理员 |
| kecheng | 课程信息 | kecheng_uuid_number 课程编号、xuenfen_number 学分 | 管理员 |
| paike | 排课信息 | kecheng_id、xingqi_types 周次、jieke_types 第几节 | 管理员 |
| xuanke | 选课信息 | kecheng_id、yonghu_id、insert_time | 学生 |
| xuankexianzhi | 选课限制 | xuankexianzhi_number 数量、kaishi_time、jieshu_time | 管理员 |
| chengji | 学生成绩 | chengji_types 成绩类型、xuenfen_number 成绩 | 教师 |
| news / dictionary | 公告 / 字典 | news_types、dic_code | 管理员 |
2.2 课程、排课、选课三张主表的建表语句
论文里最核心的三张表是课程、排课、选课,其余表都是围绕它们做人员归属和权限。下面的 DDL 直接可执行,字段名保留论文的拼音命名,方便对照论文图表答辩时一一指认。
-- 课程信息表:学分字段是后续所有上限校验的依据 CREATE TABLE kecheng ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', kecheng_uuid_number VARCHAR(32) NOT NULL COMMENT '课程编号,业务上唯一', kecheng_name VARCHAR(100) NOT NULL COMMENT '课程名称', kecheng_types INT NOT NULL DEFAULT 0 COMMENT '课程类型,对应 dictionary.code_index', xuenfen_number INT NOT NULL DEFAULT 0 COMMENT '学分,用于选课上限校验', kecheng_content VARCHAR(2000) DEFAULT NULL COMMENT '课程详情', kecheng_delete INT NOT NULL DEFAULT 0 COMMENT '假删:0 正常 1 删除', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (id), UNIQUE KEY uk_kecheng_uuid (kecheng_uuid_number), KEY idx_kecheng_types (kecheng_types) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程信息表'; -- 排课信息表:一行代表某个学期、某一周、第几节的一次上课安排 CREATE TABLE paike ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, kecheng_id INT UNSIGNED NOT NULL COMMENT '关联 kecheng.id', shangke_time DATETIME NOT NULL COMMENT '上课时间', xiake_time DATETIME NOT NULL COMMENT '结束时间', jieke_types INT NOT NULL COMMENT '第几节,对应字典', xueqi_types INT NOT NULL COMMENT '学期', xingqi_types INT NOT NULL COMMENT '周次(第几周)', paike_address VARCHAR(120) DEFAULT NULL COMMENT '上课地点', jiaoshi_id INT UNSIGNED NOT NULL COMMENT '授课教师,关联 jiaoshi.id', paike_delete INT NOT NULL DEFAULT 0 COMMENT '假删标记', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_paike_kecheng (kecheng_id), KEY idx_paike_teacher (jiaoshi_id), -- 同一教师同一时间的排课不能重复,防止排课页面重复提交 UNIQUE KEY uk_paike_conflict (jiaoshi_id, xingqi_types, jieke_types, xueqi_types) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='排课信息表'; -- 选课信息表:学生与课程的多对多关系表 CREATE TABLE xuanke ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, kecheng_id INT UNSIGNED NOT NULL COMMENT '所选课程', yonghu_id INT UNSIGNED NOT NULL COMMENT '选课学生', insert_time DATETIME NOT NULL COMMENT '选课时间,用于时间窗内外的判断留痕', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), -- 关键:同一个学生对同一门课只能有一条记录 UNIQUE KEY uk_xuanke_ke_yonghu (kecheng_id, yonghu_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课信息表';uk_paike_conflict和uk_xuanke_ke_yonghu这两个唯一索引不是可选项。排课那条把教师、周次、节次、学期组合起来约束住,管理员在页面上连点两次保存只会失败一次,不会产生两条冲突排课;选课那条是防重复选课的底线,无论前端有没有置灰按钮、后端有没有判重,最终都靠它拦住。
2.3 选课限制表的查询方式决定校验是否生效
选课限制表在论文里只列了xuankexianzhi_number(选课数量)、kaishi_time、jieshu_time三个业务字段。这套设计的隐含语义是「管理员配置一条当前生效的规则」,所以代码里不能假设表里只有一行,而要靠时间条件筛出当前那条。学分上限和已选学分的两条统计 SQL 是选课校验的全部分支来源:
-- 1) 取当前生效的选课限制:条件必须同时包含时间窗,并按 id 倒序取最新 SELECT id, xuankexianzhi_number, kaishi_time, jieshu_time FROM xuankexianzhi WHERE kaishi_time <= NOW() AND jieshu_time >= NOW() ORDER BY id DESC LIMIT 1; -- 2) 统计某学生已选总学分:必须 join 课程表,并排除已下架课程 SELECT IFNULL(SUM(k.xuenfen_number), 0) AS total_credit FROM xuanke x JOIN kecheng k ON k.id = x.kecheng_id AND k.kecheng_delete = 0 WHERE x.yonghu_id = 123; -- 3) 学生课表:选课记录 join 排课,再 join 课程拿名称 SELECT k.kecheng_name, p.xingqi_types, p.jieke_types, p.paike_address FROM xuanke x JOIN kecheng k ON k.id = x.kecheng_id JOIN paike p ON p.kecheng_id = k.id AND p.paike_delete = 0 WHERE x.yonghu_id = 123 AND k.kecheng_delete = 0 ORDER BY p.xingqi_types, p.jieke_types;第 2 条 SQL 里最容易漏的是kecheng_delete = 0。漏掉之后,管理员把某门课假删了,学生已选学分的统计里仍然包含它,结果就是明明还有额度却提示超限。第 1 条的ORDER BY id DESC LIMIT 1同样不能省,限制表在测试阶段通常会被插进好几条记录,没有排序取最新就会随机命中一条历史规则。
提示:
xuenfen_number在课程表里是「学分」,在成绩表里论文也用了同名字段表示「成绩」。两张表分开看没问题,写多表 join 时一定要带表别名,否则字段撞名报Column 'xuenfen_number' in field list is ambiguous。
3. SpringBoot 后端:选课接口的事务边界与索引兜底
3.1 依赖配置为什么能省掉一堆 XML
论文 2.4 节专门讲了 SpringBoot 的两点价值:配置损耗和依赖管理。这句话翻译到工程上就是两个注解的功劳。@SpringBootApplication内部组合了@EnableAutoConfiguration,启动时按约定加载自动配置类:类路径下有 HikariCP 和 MySQL 驱动,DataSourceAutoConfiguration就顺手把数据源建好;有 MyBatis 的 starter,MybatisAutoConfiguration就去扫@Mapper。所以对一个选课系统来说,application.yml写十来行就能跑起来,不用再手写DispatcherServlet、事务管理器和 Mapper 扫描器这三段传统 XML。
server: port: 8080 # 内嵌 Tomcat 端口,前端 devServer 代理指向这里 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/xuanke?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss # 排课时间直接按可读格式返回前端 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # yonghu_id 自动映射 yonghuIdmap-underscore-to-camel-case这一行能省掉大量resultMap,但要注意它只负责下划线转驼峰,像kecheng_uuid_number这种带多段下划线的字段,实体属性写成kechengUuidNumber才能对上。JDK 版本这里有个常见坑:老版本 SpringBoot 要求 JDK 8,新版本要求 JDK 17 起步,用低版本 JDK 去创建项目会直接在依赖解析阶段失败,选版本时先看本机java -version。
3.2 选课 Service 的事务边界
选课这个方法有四步校验和一次写入,任何一步失败都不能留下脏数据,所以@Transactional是必需的。但要说清楚它能管什么:本地事务只能保证「插入选课记录」和「后续统计更新」要么都成、要么都不成,它挡不住并发超选——两个线程同时读到已选学分 24,各自算出 24+3 没超 30,然后都去插入。真正兜住重复提交的是第 2 章建的唯一索引。
@Service public class XuanKeServiceImpl implements XuanKeService { @Autowired private XuanKeMapper xuanKeMapper; @Autowired private KeChengMapper keChengMapper; @Autowired private XuanKeXianZhiMapper xianZhiMapper; @Override @Transactional(rollbackFor = Exception.class) public void xuanKe(Integer yonghuId, Integer kechengId) { // 1) 课程必须存在且未被假删 KeCheng kc = keChengMapper.selectByIdNotDeleted(kechengId); if (kc == null) { throw new BizException("课程不存在或已下架"); } // 2) 当前是否处于选课时间窗内,取最新一条生效规则 XuanKeXianZhi xz = xianZhiMapper.selectCurrent(); if (xz == null) { throw new BizException("当前不在选课开放时间内"); } // 3) 学分上限:已选学分 + 本门学分不得超过限制数量 int used = xuanKeMapper.sumCreditByYonghu(yonghuId); if (used + kc.getXuenfenNumber() > xz.getXuanKeXianZhiNumber()) { throw new BizException("已超出选课学分上限"); } // 4) 写入,靠 uk_xuanke_ke_yonghu 兜住并发下的重复提交 try { xuanKeMapper.insert(yonghuId, kechengId, new Date()); } catch (DuplicateKeyException e) { throw new BizException("该课程已选,请勿重复提交"); } } }第 3 步的读-算-写是典型的检查后执行,并发下必然有窗口。如果压测中出现超选,常见做法有三种:把校验改成条件更新UPDATE xuankexianzhi SET ... WHERE这类带条件的原子操作;用SELECT ... FOR UPDATE锁住该学生的统计行;或者在入口加一层 Redis 预扣减,扣成功再落库。前两种改动小,适合毕设环境的单机部署;第三种要引入中间件,答辩时不加分反而增加解释成本。
第 4 步捕获DuplicateKeyException的位置有讲究:@Transactional默认只对RuntimeException回滚,而这个异常本身就是运行时异常,捕到之后转成自定义的BizException继续往上抛,事务状态标记为回滚,不会出现「提示成功但实际没写进去」的情况。如果把它吞掉改成返回 false 而不抛异常,事务就会正常提交,虽然没插入数据,但如果方法里还有其他写操作,那些写操作就留下了。
3.3 Controller 层与越权防护
登录态里的学生 ID 绝不能从请求体取。选课接口如果接受前端传来的yonghuId,任何一个学生改一下参数就能替别人选课,这是毕设系统里最容易被答辩老师问到的一处。
@RestController @RequestMapping("/api/xuanke") public class XuanKeController { @Autowired private XuanKeService xuanKeService; @PostMapping("/add") public Result add(@RequestBody XuanKeForm form) { // 学生身份从登录态解析,不信任请求体里的任何用户标识 Integer yonghuId = UserContext.getYonghuId(); xuanKeService.xuanKe(yonghuId, form.getKechengId()); return Result.ok(); } @ExceptionHandler(BizException.class) public Result handleBiz(BizException e) { // 业务异常统一转成 code=500 + 可读文案,前端直接弹 message return Result.fail(e.getMessage()); } }| 接口 | 方法 | 关键参数 | 失败场景与返回文案 |
|---|---|---|---|
| /api/xuanke/add | POST | kechengId | 课程不存在 / 不在选课时间 / 超出学分上限 / 已选过 |
| /api/xuanke/delete | POST | id | 退选时校验是否已出成绩,已出成绩不允许退 |
| /api/xuanke/my | GET | 无(取登录态) | 返回已选列表,供课表渲染 |
| /api/paike/list | GET | xueqiTypes、xingqiTypes | 按周筛选课表,假删记录不返回 |
4. Vue 前端:选课页的状态管理与前后端联调
4.1 vue.config.js 代理与 axios 拦截
前后端分离部署时,前端跑在 8081,SpringBoot 内嵌 Tomcat 跑在 8080,浏览器的同源策略会直接拦掉请求。开发阶段用 devServer 代理最省事,不用在后端加@CrossOrigin,也不会把跨域配置带到生产环境。
// vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', // 指向 SpringBoot 内嵌 Tomcat changeOrigin: true // 后端 Controller 已声明 /api 前缀,这里不做 pathRewrite } } } }axios 的响应拦截器负责把「业务失败」和「网络失败」分开处理。选课接口返回 HTTP 200 但code=500时,属于业务失败,应该弹可读文案;链接断开、超时这类才是网络失败。两种都混在一起处理,学生看到的就是一堆英文异常。
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 8000 }) request.interceptors.response.use( res => { if (res.data.code !== 200) { // 学分超限、重复选课等业务失败,直接把 msg 抛给调用处 return Promise.reject(new Error(res.data.msg || '操作失败')) } return res.data }, err => Promise.reject(new Error('网络异常,请稍后重试')) ) export default request4.2 选课按钮的状态机与重复提交
唯一索引能让数据库层不产生重复记录,但学生的体验是「点了一下没反应,又点了一下,然后弹一次成功一次失败」。所以按钮层面必须做本地节流,用submitting标志把请求锁住,成功后再刷新已选列表。
export default { data() { return { submitting: false } }, methods: { async handleXuanKe(row) { if (this.submitting) return // 本地节流,挡住连点产生的并发请求 this.submitting = true try { await request.post('/xuanke/add', { kechengId: row.id }) this.$message.success('选课成功') await this.loadMyCourses() // 成功后刷新已选列表与剩余学分 } catch (e) { this.$message.error(e.message) } finally { this.submitting = false // 无论成败都要解锁 } } } }finally里的解锁不能省,否则一次失败之后按钮就永久失效,只能刷新页面。这段逻辑和 3.2 节的DuplicateKeyException处理是配套的:前端挡住大部分连点,漏过去的那部分由唯一索引和异常翻译兜住,两层都不做就会在压测时暴露。
4.3 课表渲染与假删数据过滤
课表按周次和节次排布,前端拿到/api/paike/list返回的扁平数组后,按xingqiTypes分行、jiekeTypes分列渲染即可。这里有个容易被忽略的点:前端渲染课表用的数据来自排课表,而排课表里的paike_delete是假删字段,后端查询必须过滤掉已删除的行,前端不要再做二次过滤——前端过滤一旦和后端规则不一致,就会出现「列表里有、点进去没有」的诡异现象。
已选学分和上限的展示同理,直接调后端统计接口拿值,不要在前端用已选列表自己加总。前端的列表可能是分页的、可能漏了某门课的学分字段,自己加出来的数字和后端校验用的数字对不上,学生看到的提示就会自相矛盾。
5. 抢课压测:用 ab 复现超选并逐层定位
功能测试靠手点发现不了并发问题。论文第 6 章只做了登录功能测试和测试方法说明,真正该补的是抢课场景。准备一个 JSON 请求体文件,用 ab 打同一个学生账号的选课接口:
# xuanke.json 内容: {"kechengId": 1} # -n 总请求数,-c 并发数,-p 请求体文件,-T 内容类型 ab -n 1000 -c 200 -p xuanke.json -T application/json \ -H "Authorization: Bearer <登录后拿到的token>" \ http://localhost:8080/api/xuanke/add跑完先看Non-2xx responses这一行,它应该等于 999(1000 次请求里只有 1 次真正插入成功);再回数据库执行SELECT COUNT(*) FROM xuanke WHERE kecheng_id = 1 AND yonghu_id = 123,结果必须是 1。如果大于 1,说明uk_xuanke_ke_yonghu没建或者建错了字段顺序,这是最先要确认的一件事。Failed requests里面还会混着连接超时,把-c降到 50 再跑一遍,能把「索引失效」和「连接池不够」两种原因区分开。
排错顺序按表 5-1 走,比逐个类打断点快得多:
| 现象 | 先看哪里 | 常见原因 |
|---|---|---|
| 提示已选但列表里没有 | xuanke 表的唯一索引定义 | 之前假删的记录仍在表里,唯一索引照样生效 |
| 时间窗过期还能提交 | selectCurrent 的 SQL | 限制表有多条记录,漏了 ORDER BY id DESC LIMIT 1 |
| 学分上限算错 | sumCreditByYonghu | 漏 join 课程表,或没带 kecheng_delete = 0 |
| 压测中高频出现重复提交提示 | 前端按钮状态 / 后端异常处理 | 按钮没置灰,或 DuplicateKeyException 被吞掉没抛 |
| 压到 200 并发开始大量超时 | application.yml 的 HikariCP 配置 | 默认连接池 10,需要调 maximum-pool-size 并缩短事务 |
假删字段和唯一索引的冲突值得单独说一句。管理员把一条选课记录假删(delete = 1)之后,学生重新选同一门课会直接撞唯一索引。两种处理方式:唯一索引里带上删除标记组成三元组,代价是同一门课可能存在多条历史记录,统计学分时要额外聚合;或者在假删时把kecheng_id改写成负值做归档。毕设场景下前者改动更小,只要把统计 SQL 的 WHERE 条件改对,其余查询不受影响。
最后留一个验证用的细节:把DuplicateKeyException的日志级别调到 DEBUG,压测时观察它的抛出次数,这个数字减去成功次数,就是被唯一索引拦下来的并发请求量。这个差值直接说明索引确实在干活,而不是仅仅存在于建表语句里。
本文还有配套的精品资源,点击获取