很多同学在选课设题目的时候,都会碰到一个尴尬局面:题目看起来不难,但真到动手才发现,从前端页面到后端接口、从数据库表到部署上线,每一环都能卡住人。尤其是“软件工程课程建设网站”这类题目,听起来就是“管理系统”的老套路,可实际涉及的角色权限、课程资源、作业批改、公告通知这些模块,各自都有不少细节。我这次就把自己做这套Springboot课程建设网站的全过程拆开讲一遍,包括技术选型、数据库设计、核心功能实现、调试部署以及那些踩过的坑,都给记录下来,给正在做类似选题的同学一点参考。
这套系统本质上是给“软件工程”这门课专门做的一个教学辅助平台。学生能登录看课程资料、下载课件、提交作业、查看成绩;教师能上传课件、布置作业、批改作业、发布公告;管理员负责维护用户、课程分类、基础数据。角色虽然只有三种,但业务边界要理清楚,所以我在设计上花了不少时间在权限划分上面。这个项目最大的特点是“教学场景闭环”——从课程内容展示,到作业流转,再到成绩反馈,一条线走完。
1. 项目整体设计与思路拆解
1.1 核心需求与功能边界
做课程设计最容易犯的错,就是一上来就写代码,结果页面做了一半发现业务逻辑对不上,又推翻重来。我在动手之前,先把需求按角色拆了一遍,列了一张表:
| 角色 | 核心功能 | 说明 |
|---|---|---|
| 学生 | 查看课程、下载课件、提交作业、查看成绩、查看公告 | 学生只操作“与自己相关”的数据 |
| 教师 | 维护课程资料、管理课件、布置作业、批改作业、发布公告 | 教师管的是“自己教的课程” |
| 管理员 | 用户管理、院系/专业管理、课程管理、数据统计 | 管理员不参与具体教学业务 |
这样拆完以后,功能边界清楚了很多。比如“上传课件”这个动作,学生不会用到,教师上传时还必须要绑定课程。如果一开始没把这个归属关系想清楚,后面写SQL的时候会很痛苦。
1.2 技术选型:为什么是Springboot
选Springboot做课设,最大的优势是“省心”。它不像SSH那样要手动配一大堆XML,也不像Servlet开发那样每个请求都要写一堆样板代码。Springboot自带的自动配置、内嵌Tomcat、Spring Data JPA/MyBatis集成,基本能把环境搭建的时间压缩到半小时以内。而且现在主流教程全在讲Springboot,遇到问题搜一圈就能找到答案。
在持久层框架上,我选了MyBatis-Plus。原因有三个:第一,它的BaseMapper自带增删改查,课设级别的操作用不到复杂的SQL;第二,分页插件写起来简单,管理端列表页很好用;第三,代码生成器能直接根据数据表生成实体类、Mapper、Service,省掉大量重复劳动。模板引擎用了Thymeleaf,配合Bootstrap做页面,没有前后端分离那么重,课设答辩也更容易讲清楚逻辑。
前端没有用Vue那套,主要考虑是课程建设的业务页面主要是表格加表单,服务端渲染完全够用,而且不需要单独处理跨域和Token鉴权的问题。整个项目结构干净,演示的时候浏览器直开就能跑。
1.3 项目目录与分层规划
项目包结构我按经典的三层架构来拆:
controller:接收请求,参数校验,返回视图或JSONservice:业务逻辑,事务控制mapper:数据库访问entity:数据库实体映射config:配置类(拦截器、文件上传、跨域配置)common:统一返回结果、异常处理、工具类
entity里按模块又分了User、Course、CourseResource、Homework、HomeworkSubmit、Announcement这些实体。提前把包结构建好,后面写代码就会很顺,不会出现“这个类应该放哪”的纠结。
2. 数据库设计与核心表结构
2.1 数据库设计思路
课程建设网站的数据模型,重点是搞清楚“谁拥有什么数据”。比如课件属于某个课程,作业属于某个课程并且由某个教师创建,作业提交记录属于某个学生和某个作业。这些都是典型的一对多关系。
我设计了一张比较核心的ER关系,用文字描述就是这样:
- 用户表(用户ID、用户名、密码、姓名、角色、院系、专业、班级)
- 课程表(课程ID、课程名称、课程编号、教师ID、课程简介、上课时间、学分)
- 课件表(课件ID、课程ID、文件名称、文件路径、上传时间)
- 作业表(作业ID、课程ID、作业标题、作业内容、截止时间、创建时间)
- 作业提交表(提交ID、作业ID、学生ID、提交内容、附件路径、提交时间、得分、评语)
- 公告表(公告ID、标题、内容、发布时间、发布人)
这几张表之间用外键逻辑关联,数据库层面没建物理外键,而是在业务层通过ID做关联。对于课设级项目,不建物理外键反而让插入和删除更灵活,不容易被外键约束卡住。
2.2 核心建表语句示例
用户表的建表语句我做了精简版,你们可以直接参考:
CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` varchar(20) NOT NULL COMMENT '角色:ADMIN/TEACHER/STUDENT', `department` varchar(100) DEFAULT NULL COMMENT '院系', `major` varchar(100) DEFAULT NULL COMMENT '专业', `class_name` varchar(100) DEFAULT NULL COMMENT '班级', `email` varchar(100) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';这里有个细节:role字段用英文枚举字符串,页面展示的时候再映射成中文。这么做的好处是代码里判断角色的时候不用做字符串转换,直接ROLE_TEACHER.equals(user.getRole())就行。
作业表和提交表要注意时间字段。截止时间用datetime,提交时间和得分都放在提交表里,这样教师批改完以后能查出谁交了谁没交,顺便统计迟交的情况。很多同学会在作业表里直接加一个“已交人数”字段,这个我建议不要,人数通过count查询得到就行,存冗余字段容易出现数据不一致。
2.3 数据库设计避坑建议
- 所有表都加
create_time,排查数据问题的时候特别有用。 - 大字段单独放,不要都塞在主表里。比如课程简介这种比较长的文本,可以单独放字段,不要和核心数据混在一起。
- 文件上传的路径字段存相对路径,不要存绝对路径。部署到服务器上路径一变,之前的记录全部失效。
- 字符集统一用utf8mb4,否则用户输入的表情符号会变成问号。
3. 后端核心功能实现细节
3.1 登录认证与拦截器设计
课设网站的登录功能,如果做得太复杂(比如引入Spring Security加JWT),对答辩来说反而难讲。我用了相对轻量的方式:登录成功后把用户信息存到Session里,同时写一个拦截器校验Session。
拦截器的核心代码长这样:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { // 判断是否ajax请求 String requestedWith = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestedWith)) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); } else { response.sendRedirect(request.getContextPath() + "/login"); } return false; } return true; } }这里要处理的坑是:ajax请求如果返回了页面,前端会直接把页面HTML塞到某个div里,很难看。所以我在拦截器里判断了请求头,对ajax请求返回JSON,对普通页面请求重定向到登录页。这个小细节在课设演示的时候很加分。
拦截器还需要放行登录页、静态资源、以及学生注册接口。注册功能要控制在哪个角色能注册,不能放一个公开入口让所有人都能注册成教师。
3.2 课程管理模块的实现
课程管理是整个系统的骨架,因为课件、作业、公告全都要挂在课程下面。教师创建课程的时候,我做了两层校验:第一层是前端校验,课程名称不能为空;第二层是后端校验,同一教师下不能出现重复的课程编号。
在Service层我用了一个比较细的操作流程:
- 先根据课程编号查重。
- 创建课程记录,把教师ID设置成当前登录用户的ID。
- 同时创建一条初始公告,内容是“欢迎学习本课程”,方便学生端看到反馈。
- 处理课程封面图片的上传,没有图片就用默认图。
这里有个心得:不要把业务逻辑全堆在Controller里。比如创建课程的同时要初始化公告,这个动作是事务性的,如果放在Controller里写两步操作,中途出错就会导致课程建了但公告没建。放到Service层加@Transactional,一步到位。
3.3 作业提交与批改的业务流转
提交作业这个功能,学生端和教师端的交互流程比较关键。学生提交的时候要做三重校验:
- 作业是否存在,是否属于当前学生登录状态下的课程。
- 当前时间是否在截止时间之前。
- 该学生是否已经提交过,如果是重复提交,就要更新提交记录而不是再插一条。
我把提交和批改的状态用一张表管理。提交记录里新增了一个字段status,0表示已提交未批改,1表示已批改。这样学生端显示“待批改”“已打分”就很简单,一个字段搞定。教师批改的时候只需要更新得分和评语,再把状态置成1。
批量功能上,我做了教师端的“已提交/未提交统计”,其实就是一条带条件的分组查询SQL:
<select id="countSubmitStatus" resultType="map"> SELECT CASE WHEN hs.id IS NULL THEN 0 ELSE 1 END AS submitted, COUNT(*) AS cnt FROM sys_user u LEFT JOIN homework_submit hs ON hs.student_id = u.id AND hs.homework_id = #{homeworkId} WHERE u.role = 'STUDENT' GROUP BY submitted </select>这条SQL的重点在于LEFT JOIN和CASE WHEN,先把所有学生查出来,再关联作业提交表,这样没提交的学生也会出现在结果里,配合分组能算出提交和未提交的人数。这种方式比用NOT IN性能稍好,语义也更清楚。
3.4 文件上传功能的处理
课件上传、作业附件上传、头像上传,这三处都涉及文件操作的逻辑。我抽了一个公共的FileService,统一处理文件保存、文件大小校验、文件类型限制和文件访问映射。
关键配置在application.yml里:
spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB文件保存路径需要注意,我建议用配置项来控制:
custom: upload-path: ./upload/然后在代码里用@Value注解读取,千万别在代码里写死D:/xxx这种绝对路径。项目换一台电脑运行就找不到文件了,这个教训我从课设阶段就记住到现在。
文件下载的时候,要设置响应头Content-Disposition,不然浏览器遇到PDF或者图片会直接打开而不是下载。代码片段如下:
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));这里还有个容易踩的坑:文件名如果是中文,不编码的话会乱码。我一开始没处理,学生下载课件时文件名全是“____.pdf”,后来加了URLEncoder.encode才正常。
4. 前端页面与交互逻辑组织
4.1 页面风格与布局规划
前端用了Bootstrap 4加Thymeleaf模板,页面整体分成三个部分:登录页、学生端、教师端。管理端我合并进了教师端的布局,通过菜单权限控制显示。整体配色选了蓝色调,改一下Bootstrap的CSS变量,看起来比默认样式舒服不少。
教师端和学生端共用一套布局模板,导航栏根据角色动态渲染。这块用Thymeleaf的sec:authorize或者简单的th:if="${session.loginUser.role == 'TEACHER'}"就能控制。课设项目不用太追求前端工程化,服务端条件判断最简单直接。
4.2 课程列表的展示逻辑
学生登录后第一眼看到的是课程列表。列表要展示课程名、授课教师、学分、课程简介。我在Controller里组装了一个CourseVO对象,把教师姓名冗余进去,避免前端再发一次请求去查教师信息。这个过程在Service层做,循环查询可以接受,但要注意避免N+1问题。
举个例子,如果课程表查出10条,每查一条课程就去查一次教师信息,那就是10次额外查询。对于课设来说可接受,但如果想优化一点,可以用IN查询把所有教师一次查出来,放到Map里再匹配。我实际写的时候就用的后一种方式,代码量也不多:
List<Course> courses = courseService.list(); Set<Long> teacherIds = courses.stream().map(Course::getTeacherId).collect(Collectors.toSet()); List<User> teachers = userService.listByIds(teacherIds); Map<Long, User> teacherMap = teachers.stream().collect(Collectors.toMap(User::getId, Function.identity()));这个思路在列表页做关联字段展示时非常实用。课设答辩的时候,被问到“如何优化查询性能”,能说出避免N+1查询这个点会加分不少。
4.3 表单校验与交互体验
后端接口需要做参数校验,前端也要做一层。我在前端用jQuery Validation插件做了必填项校验和长度限制。比如注册页面,用户名必须5到20位,密码必须6位以上,两次密码要一致。这些规则能挡住大部分无效提交,后端就可以专注在业务校验上。
交互体验方面有几个小细节可以注意:
- 提交作业成功后,跳转到作业详情页并把状态展示为“等待批改”。
- 删除操作必须加二次确认弹窗,防止误删。
- 文件上传显示进度和上传成功后预览文件名。
- 分页排序统一按创建时间倒序,保证数据展示一致性。
这些细节不复杂,但能让老师答辩演示时觉得这个系统完成度比较高。
5. 调试部署与常见问题排查
5.1 本地环境搭建步骤
开发环境这块我踩过不少坑。很多同学拿到代码以后不是业务报错,而是环境和依赖的问题。我的建议是按这个顺序来:
- 安装JDK 1.8(不要装JDK 17,很多旧版框架和插件不兼容)。
- 安装Maven 3.6+,配置阿里云镜像,不然依赖下载能让人等到崩溃。
- 安装MySQL 5.7或8.0,记得密码不要设置太复杂,方便本地调试。
- 用Navicat或命令行执行项目自带的
init.sql,初始化数据库。 - 在IDEA中导入项目,等待Maven下载依赖完成。
- 修改
application.yml中的数据库地址、账号、密码。 - 运行启动类访问
http://localhost:8080。
这里有个非常关键的点:application.yml里数据库连接的URL一定要带上useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai。缺少serverTimezone,MySQL 8.0的驱动会直接报时区错误;缺少useUnicode,中文插入数据库会乱码。这两个问题出现频率极高。
5.2 前端页面加载不出来的排查思路
启动项目后,页面打不开或样式丢失,先按这个顺序排查:
- 访问
http://localhost:8080/login看是否出现登录页,如果报错误说明启动没成功,看控制台日志。 - 如果页面出来了但CSS完全没样式,按F12看控制台,多半是静态资源路径错了或拦截器放行配置不对。
- 如果后台管理页面每个请求都跳回登录页,注意检查Session的有效期和拦截器的url配置。
我当时踩过最坑的一个问题就是:Thymeleaf模板改完以后,刷新页面完全不生效。后来发现是application.properties里需要设置:
spring.thymeleaf.cache=false改成false以后,每次刷新都会重新加载模板,开发效率提升很多。生产环境再改回true就行。
5.3 数据库连接失败与端口占用
启动时报Cannot create PoolableConnectionFactory,先做三件事:
- 检查MySQL服务有没有启动。Windows下在服务管理里找MySQL,或者直接任务管理器看进程。
- 检查账号密码和URL里的数据库名是否正确,尤其是
localhost:3306/数据库名这里别拼错。 - 如果MySQL是8.0以上版本,确认驱动变成
com.mysql.cj.jdbc.Driver,旧版的com.mysql.jdbc.Driver在新驱动里可能不兼容。
端口占用问题也很常见,Springboot默认8080,如果被其他程序占用了,要么杀掉占用进程,要么在application.yml里改端口:
server: port: 8081这个简单,但很多时候容易被忽略。我建议出现端口占用时,先运行netstat -ano | findstr 8080看是哪个进程在占用,确认是不是自己之前在IDEA里没有关干净的旧项目。IDEA里旧实例没有停掉的情况很常见,多实例同时跑,页面就会访问到旧的那份。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 启动报找不到数据源 | 数据库连接配置错误 | 核对账号、密码、库名、时区参数 |
| 登录后页面循环跳登录页 | Session丢失或拦截器配置错误 | 检查拦截器放行路径,确认写入Session的key一致 |
| 中文乱码 | 数据库字符集不是utf8mb4或连接URL缺编码参数 | 统一字符集为utf8mb4,URL加characterEncoding=UTF-8 |
| 上传文件报文件大小超限 | 默认最大1MB | 在yml中调大max-file-size |
| 下载文件名乱码 | 未进行URL编码 | 加URLEncoder.encode |
| 页面改了不更新 | Thymeleaf缓存未关闭 | 开发环境设置spring.thymeleaf.cache=false |
| 列表加载慢 | N+1查询 | 批量查询后组装Map |
5.5 打包部署的基本操作
课设通常做到本地能跑就差不多了,但有些老师会要求部署到云服务器。我补充一下最简单的部署流程,方便大家扩展:
用Maven打包:
mvn clean package -DskipTests打包成功后在target目录下会生成一个jar文件。把这个jar上传到服务器,加上--spring.profiles.active=prod指定生产环境的配置。服务器上需要有JDK和MySQL,MySQL的数据库初始化sql先执行一遍。然后通过:
nohup java -jar course-website-0.0.1-SNAPSHOT.jar > app.log 2>&1 &命令以后台方式启动。这里有个经验:日志输出到文件以后,报错排查就靠tail -f app.log,别关掉终端就干瞪眼。
6. 避坑经验与实操干货
6.1 密码加密别用MD5
密码存储一定要做加密。我见过很多课设源码直接明文存密码,或者用了MD5加密。MD5现在已经不算安全了,关键是MessageDigest算出来的结果没有盐,相同密码会得到相同摘要。我自己用的是Spring自带的BCryptPasswordEncoder,它每次加密同一个密码会生成不同结果,因为内部自动加了随机盐。校验的时候调用matches方法即可:
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encoded = encoder.encode(rawPassword); boolean matched = encoder.matches(rawPassword, encoded);这个方案的好处是安全,自己写登录校验也简单。答辩时被问到安全措施,可以说出加盐哈希的原理,比说“MD5加密”高端不少。
6.2 用VO封装页面数据,别直接裸奔Entity
Entity类直接返回给前端这件事,课设阶段做起来方便,但确实有一些隐患。比如用户表里的密码字段,如果序列化返回给前端,数据就泄漏了。我做了统一的Result.java返回结构,把code、msg、data封装起来。密码字段在User实体上加了@JsonIgnore,防止接口返回时带出敏感信息。
对于需要多表关联数据的页面,我单独建了VO类,比如HomeworkSubmitVO,在Service层把作业信息、学生姓名、课程名组装好。这个做法让Controller的代码极其简洁,演示起来也容易讲。
6.3 事务管理:哪里该加@Transactional
说一个我实际遇到过的问题:教师在创建课程的时候,不仅要插入课程记录,还要初始化默认公告和默认资源目录。这个操作如果分三步写,中间一旦报错,数据库就可能出现只有课程没有公告的脏数据。我在这类“多步写入”的方法上统一加了@Transactional(rollbackFor = Exception.class)。
注意rollbackFor一定要写,如果不写,默认只有RuntimeException和Error才会触发回滚,普通的检查异常不会回滚。这是很多初级开发者忽略的细节。调试的时候可以在方法内手动抛一个异常,看数据是否回滚,验证事务配置是否正确。
6.4 分页查询的一个经验
列表页分页我用了MyBatis-Plus的分页插件。配置很容易,几行代码:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页查询用Page<User> page = new Page<>(current, size),然后Mapper方法传入Page参数即可。这里有个细节:分页插件一定要配置DbType,不然有些方言SQL会生成错误。另外,分页参数的current直接从1开始计数,前端传页码时别搞错从0开始,不然第一页数据会查不到。
6.5 源码阅读与二次开发的建议
拿到一套课设源码之后,第一件事不是急着跑起来,而是完整读一遍项目结构。看pom.xml知道用了哪些依赖,看resources目录下的配置文件知道数据库连接方式和参数,看controller层知道都有哪些接口。等你跑通一遍,再根据课设要求去改。
我这里特别要说一个点:很多同学拿到带数据库的源码,直接导入就运行,出错了也不知道怎么改。这个思路不对。正确方式应该是先看数据库初始化脚本,在本地把库建好,然后确定账号密码,最后再启动项目。配套的SQL脚本一定要自己手动执行一遍,别依赖自动执行机制。
7. 从课设到实际项目的扩展思考
这套系统做完以后,我有几个特别深的感触。第一,课程建设网站这类“管理系统”型项目,业务复杂度不高,但工作量一点也不小,关键在于把每一个模块的职责划分清楚。第二,Springboot确实把Java Web开发的工程量压缩到了一个很适合学生上手的程度,但如果不理解底层原理,遇到问题很容易抓瞎。第三,调试部署的能力,往往是真正拉开差距的地方。你功能写得再好,部署不起来,到答辩的时候还是零分。
我现在回头看自己当初做这套系统的时候,最庆幸的是没有直接拿网上源码抄一遍交差。从数据库设计到每一条SQL、每一个Controller方法,全部手写一遍,踩过的坑全都变成了经验。到了调试阶段出了问题,我能一眼判断大概是哪个环节的锅。这种能力不是看教程能学来的,必须自己动手磨一遍。
如果你们正在做类似的题目,我建议在“软件工程课程建设网站”这个需求上,多想一想还能扩展什么。比如加入课程评价功能、学习进度统计、公告的定时发布,这些都是复杂度可控又能体现工作量的小功能,挺适合在答辩里作为亮点来展示。
这个项目的后续方向其实也挺多。比如把前端换成Vue3,做成前后端分离;把文件存储从本地换成OSS对象存储;或者把登录改成OAuth2统一认证。每一步改动都能学到新东西,比重复造轮子有价值。以上是我做这个项目过程中的全部心得,希望能给正在为课程设计发愁的同学一点参考。
最后再分享一个我个人的操作习惯:写代码前先把接口文档列出来,哪怕只是一张纸。简单写下URL、参数、返回值,整个系统的结构就自动浮现了,开发过程中特别省脑力。你们可以试试看。