Spring Boot课程建设网站开发全流程实战解析
很多同学在选课设题目的时候都会碰到一个尴尬局面题目看起来不难但真到动手才发现从前端页面到后端接口、从数据库表到部署上线每一环都能卡住人。尤其是“软件工程课程建设网站”这类题目听起来就是“管理系统”的老套路可实际涉及的角色权限、课程资源、作业批改、公告通知这些模块各自都有不少细节。我这次就把自己做这套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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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;charsetUTF-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 作业提交与批改的业务流转提交作业这个功能学生端和教师端的交互流程比较关键。学生提交的时候要做三重校验作业是否存在是否属于当前学生登录状态下的课程。当前时间是否在截止时间之前。该学生是否已经提交过如果是重复提交就要更新提交记录而不是再插一条。我把提交和批改的状态用一张表管理。提交记录里新增了一个字段status0表示已提交未批改1表示已批改。这样学生端显示“待批改”“已打分”就很简单一个字段搞定。教师批改的时候只需要更新得分和评语再把状态置成1。批量功能上我做了教师端的“已提交/未提交统计”其实就是一条带条件的分组查询SQLselect idcountSubmitStatus resultTypemap 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层做循环查询可以接受但要注意避免N1问题。举个例子如果课程表查出10条每查一条课程就去查一次教师信息那就是10次额外查询。对于课设来说可接受但如果想优化一点可以用IN查询把所有教师一次查出来放到Map里再匹配。我实际写的时候就用的后一种方式代码量也不多ListCourse courses courseService.list(); SetLong teacherIds courses.stream().map(Course::getTeacherId).collect(Collectors.toSet()); ListUser teachers userService.listByIds(teacherIds); MapLong, User teacherMap teachers.stream().collect(Collectors.toMap(User::getId, Function.identity()));这个思路在列表页做关联字段展示时非常实用。课设答辩的时候被问到“如何优化查询性能”能说出避免N1查询这个点会加分不少。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一定要带上useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai。缺少serverTimezoneMySQL 8.0的驱动会直接报时区错误缺少useUnicode中文插入数据库会乱码。这两个问题出现频率极高。5.2 前端页面加载不出来的排查思路启动项目后页面打不开或样式丢失先按这个顺序排查访问http://localhost:8080/login看是否出现登录页如果报错误说明启动没成功看控制台日志。如果页面出来了但CSS完全没样式按F12看控制台多半是静态资源路径错了或拦截器放行配置不对。如果后台管理页面每个请求都跳回登录页注意检查Session的有效期和拦截器的url配置。我当时踩过最坑的一个问题就是Thymeleaf模板改完以后刷新页面完全不生效。后来发现是application.properties里需要设置spring.thymeleaf.cachefalse改成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缺编码参数统一字符集为utf8mb4URL加characterEncodingUTF-8上传文件报文件大小超限默认最大1MB在yml中调大max-file-size下载文件名乱码未进行URL编码加URLEncoder.encode页面改了不更新Thymeleaf缓存未关闭开发环境设置spring.thymeleaf.cachefalse列表加载慢N1查询批量查询后组装Map5.5 打包部署的基本操作课设通常做到本地能跑就差不多了但有些老师会要求部署到云服务器。我补充一下最简单的部署流程方便大家扩展用Maven打包mvn clean package -DskipTests打包成功后在target目录下会生成一个jar文件。把这个jar上传到服务器加上--spring.profiles.activeprod指定生产环境的配置。服务器上需要有JDK和MySQLMySQL的数据库初始化sql先执行一遍。然后通过nohup java -jar course-website-0.0.1-SNAPSHOT.jar app.log 21 命令以后台方式启动。这里有个经验日志输出到文件以后报错排查就靠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封装页面数据别直接裸奔EntityEntity类直接返回给前端这件事课设阶段做起来方便但确实有一些隐患。比如用户表里的密码字段如果序列化返回给前端数据就泄漏了。我做了统一的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; } }分页查询用PageUser 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、参数、返回值整个系统的结构就自动浮现了开发过程中特别省脑力。你们可以试试看。