Spring Boot毕设项目实战解析:从CRUD到完整科普平台
简介一份基于Spring Boot框架的冬奥会科普平台毕业设计资料覆盖Java、Spring Boot、MySQL及B/S架构适合计算机专业毕业生或正在练手Spring Boot全栈项目的学习者。资源共807个文件压缩包52.12MB包含115个Java源码、45个Vue前端组件、41个HTML页面、53个CSS样式、164个JS脚本并提供1个SQL数据库脚本和2个MP4演示视频类型覆盖前后端代码与运行说明。功能上实现用户登录注册、冬奥项目展示、精彩视频、冬奥论坛、新闻浏览以及个人中心管理员端支持用户管理、项目类型管理、内容管理和系统维护。另附开发说明文档、启动/运行批处理脚本便于本地部署时快速梳理环境与排错。目前已有186人学习下载适合需要完整毕设源码和演示参考的同学。1. 一个 Spring Boot 毕设项目拆开看比想象中值钱接手或重做这类基于 Spring Boot 的科普平台时很多人第一反应是又一个 CRUD 毕设——表面上是登录注册加几个管理页但把 index.html.bak、IndexMain.vue.bak 这些文件铺开看前后端分离的结构、用户与管理员双角色鉴权、视频与论坛的内容管理、以及三个 .bat 脚本串联的构建链路全都在里面。它解决的典型问题是科普内容零散在静态页面里、用户只能看不能参与讨论、管理员想改一条新闻得直接动代码。对刚接触 Spring Boot 全栈的开发者和需要快速搭出完整毕业设计的工程师来说这套东西的价值在于你能在一个项目里同时看到表现层、业务层和数据层是怎么拧在一起的。下面按一条可以直接跑通的路径把骨架、鉴权、内容管理和部署排错一次讲透。2. 项目骨架与架构选型.bat 脚本、包结构和 MySQL 表设计2.1 三个批处理脚本背后的构建思路项目根目录放着的 1-install.bat、2-run.bat、3-build.bat是给评审老师和接手同学准备的傻瓜入口。install 做的事在 Maven 工程里等价于清理并安装依赖run 负责本地启动build 打可部署产物。install 脚本核心逻辑一般是echo off cd /d %~dp0 call mvn clean install -DskipTests echo echo dependency installed, ready to run pause这里cd /d %~dp0把工作目录切到脚本所在位置防止双击运行时因为当前路径不对导致 Maven 找不到 pom.xml。-DskipTests跳过测试执行但保留测试编译毕设阶段没有 CI 流水线的情况下这个参数能规避掉大量因本机环境差异造成的构建失败。三个脚本按编号执行本质上就是一条人工流水线装依赖、起服务、出包。run 脚本执行的是mvn spring-boot:run本地会起一个内嵌 Tomcat默认监听 8080 端口。如果机器上已经跑了别的服务启动日志会抛Port 8080 was already in use这不是项目代码问题改 application.yml 里的server.port或者杀进程即可。这套脚本设计本身值得借鉴——它把怎么让项目跑起来这个高频问题压缩成了三次双击。2.2 Spring Boot 包结构与数据库表设计项目按 B/S 模式组织Spring Boot 后端提供 REST 接口Vue 前端负责渲染交互。后端包结构是 Spring Boot 项目里最常见的一种分层方式com.example.olympic ├── controller // 接口层只做参数接收和结果返回 ├── service // 业务层放核心逻辑 ├── mapper // MyBatis 数据访问层 ├── entity // 实体类与数据库表字段一一对应 ├── config // 配置类、拦截器注册、CORS └── common // 统一返回结果 Result、异常处理数据库按功能模块拆表主要包含 user用户、project_type项目类型、winter_project冬奥项目、video精彩视频、forum_post论坛帖子、news新闻六张核心表。其中 user 表是鉴权的基础字段设计直接影响后面登录注册的写法CREATE TABLE user ( id BIGINT(20) NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(255) NOT NULL COMMENT BCrypt 加密后的密码, role VARCHAR(20) DEFAULT user COMMENT 角色: user/admin, nickname VARCHAR(50) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码字段设置成 255 而不是 50 是有讲究的BCrypt 加密结果固定 60 位但未来如果要换成加盐策略或升级算法长度不够会直接报Data too long for column。用户名加唯一索引注册时靠数据库做兜底约束比只靠代码判断稳妥。表关系上winter_project 通过type_id关联 project_typevideo 通过project_id关联项目前台按类型筛选时只需要一次 join。2.3 持久层选型MyBatis-Plus 省掉什么毕设项目里最常见的 Mapper 写法是继承 MyBatis-Plus 的BaseMapperT单表 CRUD 完全不需要写 SQL 文件Mapper public interface UserMapper extends BaseMapperUser { }继承之后selectById、selectOne、selectPage、insert这些方法直接可用。springboot 项目里用 MyBatis-Plus 的价值在于单表操作用内置方法多表关联才写 XML代码量能砍掉一半。比如前台首页要同时拿项目名和类型名这种 join 查询就得放在 XML 里select idselectProjectWithType resultTypemap SELECT p.*, t.name AS type_name FROM winter_project p LEFT JOIN project_type t ON p.type_id t.id WHERE p.status 1 ORDER BY p.sort_order ASC /select这里用LEFT JOIN而不是INNER JOIN原因是科普平台的内容宁可类型名显示为空也不能因为类型被删除导致整条项目记录从列表消失。ORDER BY p.sort_order保证管理员配置的排序权重在前台生效。这种细节是评审老师最爱问的也是实际项目里最常见的踩坑点。3. 登录注册与角色鉴权BCrypt、Session 和拦截器3.1 注册接口密码处理是第一个扣分点登录注册是评审答辩时必看的模块。第一版常见错误是密码明文入库一旦数据库泄露所有账号直接暴露。这个项目采用 BCryptPasswordEncoder它比 MD5 加盐更省事的点在于盐值混在加密结果里校验时一次调用就能完成不需要额外维护盐字段。Service public class UserServiceImpl implements UserService { Resource private UserMapper userMapper; Resource private PasswordEncoder passwordEncoder; Override public Result register(RegisterDTO dto) { Long count userMapper.selectCount( new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (count 0) { return Result.error(用户名已存在); } User user new User(); user.setUsername(dto.getUsername()); user.setPassword(passwordEncoder.encode(dto.getPassword())); user.setRole(user); userMapper.insert(user); return Result.ok(); } }LambdaQueryWrapper是类型安全的查询条件构造器eq对应 SQL 里的User::getUsername是方法引用字段名写错编译期直接报错比字符串拼接稳得多。查重用selectCount而不是把记录查出来再判断减少传输数据量。注意passwordEncoder.encode()在注册时只执行一次同一个密码每次加密结果都不同这是 BCrypt 随机盐的特性不要因为两次加密结果不一致就以为是 bug。3.2 登录接口角色字段的校验顺序登录接口接收用户名、密码、角色三个字段。角色由前端下拉框传入这决定了登录后跳转到前台首页还是管理后台。后端拿到角色后必须校验该账号确实拥有对应权限否则普通用户手动改请求体就能进后台PostMapping(/login) public Result login(RequestBody LoginDTO loginDTO, HttpSession session) { User user userMapper.selectOne( new LambdaQueryWrapperUser() .eq(User::getUsername, loginDTO.getUsername())); if (user null || !passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } if (!user.getRole().equals(loginDTO.getRole())) { return Result.error(所选角色与账号权限不匹配); } session.setAttribute(loginUser, user); return Result.ok(user); }这里两个 if 的顺序是刻意的先做用户名密码校验再做角色匹配校验。如果把角色校验放前面攻击者可以通过该角色不匹配和密码错误两种报错区分账号是否存在属于典型的信息泄露。passwordEncoder.matches(rawPassword, encodedPassword)第一个参数是前端传来的明文第二个是库里存的密文方法内部自动从密文提取盐值进行比对无需关心加密细节。3.3 角色拦截器与前端路由控制后台接口不能裸奔拦截器统一做权限控制。AdminInterceptor实现了HandlerInterceptor接口核心逻辑在preHandle里Component public class AdminInterceptor 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) { response.setStatus(401); return false; } if (!admin.equals(user.getRole())) { response.setStatus(403); return false; } return true; } }401 和 403 必须区分开未登录返回 401前端拦截到后跳转登录页已登录但角色不是 admin 返回 403前端统一弹无权限提示。前端 axios 响应拦截器里根据这两个状态码做不同处理。拦截器要注册进 WebMvcConfigurer 才能生效Configuration public class WebConfig implements WebMvcConfigurer { Resource private AdminInterceptor adminInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(adminInterceptor) .addPathPatterns(/api/admin/**) .excludePathPatterns(/api/auth/login, /api/auth/register); } }/api/admin/**里的双星号匹配任意层级路径后续新增管理员接口不需要改配置。登录注册接口通过excludePathPatterns排除。这里没有用 Spring Security因为这个项目只有两种角色、没有细粒度权限点拦截器加 Session 的方案更直观也更好在答辩时讲清楚。4. 前台与后台内容管理分页、视频和论坛模块4.1 统一返回结构 Result 与分页接口管理员后台管理的对象明确用户、项目类型、冬奥会项目、精彩视频、论坛内容。六个模块的接口形态高度一致都依赖两层基础设施统一返回封装和分页查询。Result 类的设计如下Data public class Result { private Integer code; private String message; private Object data; public static Result ok() { return new Result(200, success, null); } public static Result ok(Object data) { return new Result(200, success, data); } public static Result error(String message) { return new Result(500, message, null); } }前端 axios 响应拦截器统一判断res.data.code 200业务异常和系统异常在 UI 层表现一致不用每个页面单独写错误处理。分页接口以冬奥会项目管理为例GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword) { LambdaQueryWrapperWinterProject wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(WinterProject::getName, keyword); } wrapper.orderByAsc(WinterProject::getSortOrder); PageWinterProject result winterProjectService.page(new Page(page, size), wrapper); return Result.ok(result); }like会生成WHERE name LIKE %关键字%关键词为空时不能拼接这个条件否则全表扫描。orderByAsc按 sort_order 排序保证管理员配置的推荐顺序在前台生效。MyBatis-Plus 的 Page 对象自带 total、records 字段前端拿到后直接渲染 el-pagination 分页条不需要自己拼 count 查询。4.2 视频表设计存相对路径还是完整 URL精彩视频管理包含视频标题、封面、视频地址、所属项目和发布时间。核心取舍在视频地址字段的存储方式字段类型说明idbigint主键titlevarchar(100)视频标题covervarchar(255)封面图路径video_urlvarchar(255)视频文件路径project_idbigint关联冬奥项目 idcreate_timedatetime发布时间推荐存相对路径而不是完整 URL比如/upload/videos/2024/01/skiing.mp4。原因是环境迁移时 IP 和端口会变存完整 URL 意味着改环境必须改数据库。前端通过VITE_APP_BASE_URL videoUrl拼接完整访问地址后端用静态资源映射把 upload 目录暴露出去Configuration public class UploadConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: System.getProperty(user.dir) /upload/); } }System.getProperty(user.dir)取的是项目启动目录这样视频文件跟着项目走打包移到服务器后只需保证 upload 目录一并拷贝。论坛模块则拆成帖子表和回复表回复表通过post_id关联帖子帖子表记录浏览量字段。浏览量的更新策略上前台点击帖子详情时用UPDATE forum_post SET view_count view_count 1 WHERE id ?直接累加不走先查后改的两步操作避免并发下丢失更新。4.3 前台首页的多接口并发加载前台首页数据来源多轮播图取最新项目、冬奥项目列表按类型分组、精彩视频取热门前五、论坛取最新帖子。如果依次请求用户要等最慢的接口返回才能看到完整页面。Vue 端用Promise.all并发请求async function loadHomeData() { const [bannerRes, projectRes, videoRes, forumRes] await Promise.all([ api.get(/api/home/banners), api.get(/api/project/list), api.get(/api/video/hot), api.get(/api/forum/latest) ]); // bannerRes.data.data 赋值给轮播图 // projectRes.data.data 按 typeId 分组渲染 // videoRes.data.data 渲染推荐位 // forumRes.data.data 渲染最新讨论 }四个接口互不依赖并行请求的耗时约等于最慢接口的耗时而不是四个接口耗时的总和。后端这组接口要注意home 下的接口尽量避免联表查询比如 banner 只需要 id、标题、封面图三个字段直接select指定列减少不必要的数据传输。5. 构建打包与排错三个 .bat 的完整链路5.1 build 脚本与可执行 jar 的生成3-build.bat 是整个交付链路的最后一环它的职责是把当前源码变成可独立部署的产物echo off cd /d %~dp0 call mvn clean package -DskipTests if exist target\*.jar ( echo build ok, jar is in target directory copy target\*.jar dist\ /Y ) else ( echo build failed, check the maven output above ) pausemvn clean package先清理 target 再编译打包生成的 jar 内置 Tomcat服务器上执行java -jar olympic-platform.jar即可启动。dist 目录作为固定产物输出位置部署时只拷这一个文件夹。if exist判断的意义是Maven 构建失败时 target 下没有 jar直接 copy 会拷到旧文件加判断能避免把过期产物当新包。5.2 几个高频坑和对应解法数据库时区连接串问题出现率最高。MySQL 8 下连接串不带serverTimezone会报时间类型转换异常推荐配置spring: datasource: url: jdbc:mysql://localhost:3306/winter_olympic?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码characterEncodingutf8解决中文乱码serverTimezoneAsia/Shanghai解决时间差 8 小时问题。前后端分离部署时还有跨域问题在 WebConfig 里追加 CORS 映射Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); }注意allowCredentials(true)和allowedOriginPatterns(*)要配合使用如果写成allowedOrigins(*)在部分浏览器版本下会被拒绝。启动失败的排查顺序固定为数据库是否启动、连接账号密码是否正确、8080 端口是否被占用、Maven 依赖是否下载完整。按这个顺序十分钟内定位到根因。本文还有配套的精品资源点击获取