
每年到做计算机毕设的季节Java方向的选题来来回回就那么几类管理系统、商城系统、资讯门户占了大多数。线上美食社区与菜谱分享系统表面看也逃不出增删改查的圈子实际上它把注册登录、内容发布、文件上传、点赞收藏、评论互动、后台管理、搜索分页这些功能全串在了一个项目里正好覆盖了Spring Boot在课堂上学过但没真正串联起来的知识点。这篇文章是我自己做这套系统时的完整复盘包括需求该怎么拆、表怎么建、核心代码怎么组织、前端怎么合并进后端一起打包、部署时容易踩哪些坑适合正在做毕设或者刚开始接触Spring Boot实战的人直接参考。1. 项目定位与需求拆解1.1 这个系统解决的是什么问题美食社区和菜谱分享平台核心用户有两类。一类是普通用户想找一个能发菜谱、发图文、收藏别人做法、在上面评论交流的地方另一类是平台管理人员需要审核内容、管理用户、处理举报。计算机毕设里做这类系统本质上是在做一个内容社区的最小可行版本。结合标题里的“线上美食社区”和“菜谱分享系统”我把它拆成几个核心场景用户注册与登录区分普通用户和管理员。菜谱的发布、编辑、删除、详情展示。菜谱的分类浏览和关键词搜索。用户对菜谱的收藏、点赞、评论。用户个人信息维护包括头像、昵称、简介。后台管理端对用户、菜谱、评论的审核与管理。很多同学拿到这个题直接开始写代码结果写到一半发现功能越加越多表结构反复改前端页面和后端接口对不上。我建议先别急着建工程把以上场景一条条列清楚再确定哪些是必须做的、哪些是加分项。毕设答辩时老师不会因为你少做了一个在线聊天而挂你但如果你连登录、分页、上传这三个基础能力都讲不透反而容易翻车。1.2 用户角色与权限怎么划分这个系统里权限不需要做成复杂的RBAC模型两种角色就够了普通用户注册登录后可以浏览菜谱、发布菜谱、收藏点赞、发表评论、编辑自己的资料和菜谱。管理员登录后进入后台管理界面可以禁用或删除违规用户、下架违规菜谱、删除不当评论。在后端实现上我用了Spring Security JWT的方式。用户登录成功后服务端签发一个token返回给前端前端后续请求都带上这个token后端通过拦截器解析用户身份。管理员接口额外校验角色字段只有role1的账号可以访问。对于毕设项目不建议一开始就去搞OAuth2、短信验证码或者第三方登录这些内容在论文里写不了太多实质性的东西反而把工期拉长。做好用户名密码登录、记住登录状态、角色权限校验已经足够支撑起整个系统的安全骨架。1.3 技术选型怎么定这套系统的技术栈我最终选的是后端Spring Boot 2.7.x MyBatis Plus Spring Security JWT数据库MySQL 8.0前端Vue 2 Element UI编译后的静态文件直接放到Spring Boot的resources/static目录下缓存Redis可选用于轮播图、热门菜谱等热点数据为什么不用Spring Boot 3.x当时考虑的是兼容性问题。很多同学的电脑上装的JDK是8Spring Boot 3.0强制要求JDK 17以上如果本地环境还停留在JDK 8版本选太高会连启动都过不去更别提MyBatis Plus和部分依赖的兼容性了。Spring Boot 2.7.x配合JDK 8是我实测下来最稳的组合。为什么用MyBatis Plus而不是原生MyBatis或者JPA因为MyBatis Plus提供了单表CRUD的现成方法分页插件也内置了开发效率明显高。毕设项目里单表查询占了大部分像用户列表、菜谱列表、评论列表基本都是单表加条件的操作用MyBatis Plus能把代码量压到很小剩下精力留给业务逻辑。2. 数据库设计与建模2.1 用户表怎么设计用户表是所有业务的基础。我建表时没有偷懒直接用系统自带表而是独立设计了一张sys_user表至少包含这些字段字段名类型说明idbigint主键自增usernamevarchar(50)登录账号唯一passwordvarchar(100)密码BCrypt加密后存储nicknamevarchar(50)昵称avatarvarchar(255)头像地址gendertinyint性别0未知1男2女phonevarchar(20)手机号emailvarchar(100)邮箱roletinyint0普通用户1管理员statustinyint0正常1禁用created_timedatetime创建时间updated_timedatetime更新时间注意几点密码一定不要明文存Spring Security自带的BCryptPasswordEncoder就可以用加密串存进去即使数据库泄露了原始密码也拿不到。status字段是用来做用户禁用的管理员把某个用户状态改成1之后这个用户登录接口直接返回账号异常前端就进不去了。2.2 菜谱表与菜谱分类表菜谱是业务核心我设计了recipe表和recipe_category表。分类表很简单id、category_name、sort、created_time。菜谱表的字段要包含一个菜谱详情页需要的所有信息字段名类型说明idbigint主键user_idbigint发布者IDcategory_idbigint分类IDtitlevarchar(200)菜谱标题cover_imagevarchar(255)封面图imagestext详情图列表JSON格式存储difficultytinyint难度1简单2一般3困难cooking_timeint烹饪时长单位分钟ingredientstext食材清单JSON格式stepslongtext制作步骤JSON格式descriptionvarchar(500)简介view_countint浏览数like_countint点赞数favorite_countint收藏数statustinyint0正常1审核中2下架created_timedatetime发布时间食材清单和制作步骤为什么用JSON因为每个菜谱的食材数量不一样步骤条数也不一样用关系型表去拆会很麻烦而且查询菜谱详情时还要来回join。直接存JSON字符串在后端用Jackson解析成List再返回给前端开发最简单数据展示也更灵活。毕设论文里可以提一句“采用JSON格式存储非结构化数据减少表关联数量”反而显得有设计思考。2.3 收藏、点赞、评论的表结构这类互动功能如果只在菜谱表里放一个like_count字段不给每条点赞记录建表会有一个问题用户无法判断自己是否已经点过赞。所以必须建对应的关系表。favorite表id、user_id、recipe_id、created_time。唯一索引加在user_id和recipe_id上防止重复收藏。like表id、user_id、recipe_id、created_time。同理加唯一索引。comment表id、recipe_id、user_id、content、parent_id、created_time。parent_id用于回复某条评论如果是顶级评论就填0。每次点赞、收藏的时候先查关系表是否存在记录不存在就插入同时把菜谱表里的计数加1取消点赞再删除记录并把计数减1。写入顺序注意一下建议先更新关系表再更新计数虽然极端情况下可能短暂不一致但毕设项目里的并发量完全不需要上事务锁。评论表用parent_id来支持两级评论就够了做到楼中楼级别会明显增加前端渲染复杂度没有特殊要求别加。2.4 为什么不用外键我的表之间是有逻辑关联的比如recipe.user_id对应sys_user.id但我建表时没有加外键约束。原因很简单有外键约束的情况下删除用户时必须先处理掉对应的菜谱和评论否则数据库直接报错。用逻辑外键在Service层自己控制删除顺序开发时灵活很多也不会因为数据顺序写错就卡死。毕设答辩时如果老师问起来可以诚实说“通过逻辑关联而非物理外键来保证代码层面的可控性”这是后端开发中很常见的取舍。3. 后端核心功能实现3.1 登录认证与接口鉴权我用的方案是Spring Security JWT。简单讲一下思路用户提交用户名密码后登录接口调用AuthenticationManager进行认证认证通过后用userId和role生成token返回。前端把token存在localStorage里每次请求在请求头header里带上Authorization: Bearer token。后端定义一个拦截器或者OncePerRequestFilter从header中取出token用JWT工具类解析出用户信息放入ThreadLocal当前线程变量中。Controller里通过LoginUser注解或者工具类拿到当前用户ID。关键代码大致是这个形式Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); try { Claims claims JwtUtil.parseToken(token); Long userId claims.get(userId, Long.class); Integer role claims.get(role, Integer.class); // 把用户信息放到请求上下文方便后续获取 UserContext.set(new LoginUser(userId, role)); } catch (Exception e) { // token无效不处理由具体接口决定是否拒绝 } } filterChain.doFilter(request, response); } }使用Spring Security时有一个容易踩的坑如果不加配置Spring Security默认会拦截所有请求并且要求表单登录。所以必须重写SecurityFilterChain把登录、注册、菜谱列表、菜谱详情等公开接口放行管理端接口和需要登录才能操作的发菜谱、评论、点赞、收藏接口统一要求认证。我在实际项目中用了一个简单直接的方法http.authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/**, /api/recipe/list, /api/recipe/detail/**, /static/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() );这里注意一个细节Spring Security的hasRole默认会给角色字符串加上ROLE_前缀如果你数据库里存的角色是1和0转换时要把1转成ADMIN0转成USER。3.2 菜谱发布与图片上传菜谱发布的请求体是JSON包含标题、分类、封面、详情图数组、食材数组、步骤数组等。后端用一个RecipeDTO接收然后转成实体类存入数据库。图片上传是这套系统里比较容易被问到的点。当时我实现的方式是前端把文件通过multipart/form-data格式POST到/api/upload接口后端把文件保存到服务器本地指定目录然后返回可访问的URL。本地存储的目录我建议和项目路径分离不要直接存到resources下因为Spring Boot打成jar包后resources里的文件不方便写。我在配置文件里定义了一个绝对路径file.upload.path/data/upload/然后通过配置映射成URL访问Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath uploadProperties.getPath(); registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }这样用户上传的头像、菜谱封面、详情图片都存到/data/upload/下面前端通过/upload/xxx.jpg就能访问到。需要注意给上传文件加个大小限制Spring Boot默认单文件最大1MB做毕设一般改成10MB左右spring.servlet.multipart.max-file-size10MB spring.servlet.multipart.max-request-size50MB还要做文件类型校验只允许jpg、png、jpeg、webp等常见格式。我见过不少项目没校验类型结果上传了一个.php文件到服务器这在实际部署中是很危险的答辩时也会被追问安全相关的问题。3.3 列表分页与关键字搜索首页菜谱列表、后台用户列表、评论列表都需要分页。MyBatis Plus的分页插件配置好之后查询时直接传入当前页和每页条数即可PageRecipe page new Page(pageNum, pageSize); LambdaQueryWrapperRecipe wrapper new LambdaQueryWrapper(); wrapper.eq(Recipe::getStatus, 0) .orderByDesc(Recipe::getCreatedTime); IPageRecipe result recipeMapper.selectPage(page, wrapper);搜索功能我当时直接用了MySQL的LIKE模糊查询没上Elasticsearch因为毕设阶段菜品量和用户量都极小LIKE查询完全够用。需要注意LIKE查询会扫全表但如果加上索引和限制状态条件性能可以接受论文里可以提“后续可通过全文索引或ES扩展”。模糊搜索还有一个细节关键字要trim之后再拼进LIKE表达式并且用%通配符时要注意SQL注入问题。MyBatis Plus的LambdaQueryWrapper会自动做参数绑定基本不存在注入风险但如果你喜欢自己写XML里的${}拼接就要特别注意了。3.4 评论、点赞、收藏的实现顺序这三个功能我放在一起实现因为它们有一个共同点操作前都需要判断用户登录状态操作后都要更新菜谱表的统计字段。点赞的逻辑一般是这样从token中取userId。根据recipeId查询菜谱是否存在不存在直接返回菜谱不存在。查like表是否已有记录有则删除记录菜谱like_count减1返回“取消点赞成功”。没有则插入记录菜谱like_count加1返回“点赞成功”。评论的逻辑需要做一点防刷处理。最简单的是在前端提交后加上频繁点击限制后端也可以校验评论内容长度不能为空或超过500字。评论内容不要用富文本纯文本就够了否则XSS问题很难防。收藏的逻辑和点赞几乎一样只是业务名词不同。实现的时候建议把收藏功能也做成可以收藏、取消收藏、查看是否已收藏、展示收藏列表四个接口别只做单向添加不然前台页面没法展示“我的收藏”。4. 管理后台与前端集成4.1 管理端功能怎么做管理端不做单独的前端工程我在同一个Vue项目里通过路由和角色控制来区分用户端和管理端。登录时后端返回的token里包含角色信息前端根据角色决定路由跳转和菜单显示。管理端主要页面我做了这些仪表盘统计总用户数、总菜谱数、今日新增数。用户管理用户列表、禁用/启用用户、重置密码。菜谱管理所有菜谱列表、下架/上架菜谱。评论管理删除违规评论。管理接口统一放在/api/admin/**路径下后端配置了hasRole(ADMIN)权限这样即使普通用户摸到了接口地址也会被拦截下来。这里有一个很实际的点很多同学做完前端页面后发现普通用户还是能直接访问管理员页面原因往往是前端只隐藏了菜单没有做路由拦截或者后端接口没加权限控制。正确的做法是两层都要控制前端控制体验后端控制安全。4.2 Vue前端如何和生产环境后端一起部署毕设项目如果前端和后端分两个端口启动比如前端跑8080、后端跑9090联调时没问题但部署到服务器上或者拿给老师演示时就多一个步骤。最省事的做法是把Vue项目构建后的dist目录拷贝到Spring Boot项目的src/main/resources/static目录下重新打包前后端就在同一个端口了。具体操作是这样的在Vue项目根目录执行npm run build生成dist文件夹。把dist里的所有文件复制到Spring Boot项目的src/main/resources/static/目录。修改Vue项目里所有接口请求地址为相对路径比如/api/user/login不要写http://localhost:9090/api/user/login。重新执行mvn clean package打包。这样打出来的jar包天然就是前后端一体的部署到服务器上只开一个端口。老师在浏览器输入IP加端口就能直接看到登录页演示效果非常直观。需要注意一个问题如果Vue用了history路由模式刷新页面会出现404。因为前端路由是浏览器URL变化但服务器端没有对应的文件或接口。解决办法是后端增加一个转发把非/api路径统一转发到index.html。Spring Boot里可以写一个Controller或者实现ErrorViewResolver。毕设项目最简单的方式是把路由模式改成hash模式刷新就不会出404代价是URL里带个#号不影响演示效果。5. 部署流程记录5.1 本地开发环境的搭建把项目跑起来需要准备的工具JDK 8 或 JDK 11Maven 3.6以上MySQL 8.0IDEA 或 EclipseRedis如果用了缓存功能导入项目后先改配置文件重点改数据库连接部分spring.datasource.urljdbc:mysql://localhost:3306/food_community?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password你的密码数据库里先执行项目提供的sql脚本把表和初始数据导入。然后启动项目访问http://localhost:8080如果看到登录页面说明前后端都正常了。5.2 服务器部署注意的事项生产环境部署时服务器上要有JDK和MySQL。数据库我先用Navicat导入了本地sql文件再修改服务器上的数据库地址。如果项目配置里的密码和服务器数据库不一致一定要改成服务器的密码不要图省事保留本地密码。启动项目用java -jar food-community.jar但这样关掉窗口进程就结束了推荐用nohup方式nohup java -jar food-community.jar app.log 21 查看日志tail -f app.log如果出现端口占用用netstat -tunlp | grep 8080查一下是哪个进程杀掉或者换端口。图片上传路径在部署环境里要确认有写权限。我之前遇到过一次本地跑通部署到服务器后图片一直上传失败最后发现是/data/upload目录不存在。启动前先执行一下mkdir -p /data/upload并设置好权限能省去不少麻烦。5.3 演示视频和部署说明怎么写这类项目标题里提到的部署说明和演示视频很多同学不重视其实这是答辩和评阅中非常加分的一块。部署说明不要写成流水账要覆盖三个关键点环境要求JDK版本、MySQL版本、是否需要Redis。数据库初始化sql文件怎么执行、默认账号是什么。启动步骤前端要不要单独启动、后端怎么启动、访问地址是多少。演示视频建议控制在5到8分钟按主流程走。先展示注册登录再演示发布菜谱、上传图片、查看菜谱详情、点赞收藏评论最后进入管理端展示用户和菜谱管理。每个页面停留时间不要太长鼠标操作要流畅视频里声音清晰即可。视频的作用是让评阅老师在没有跑通代码的情况下也能快速了解系统功能完整性。6. 常见问题与排错实录6.1 数据库连接失败和字符集乱码数据库连接失败最常见的原因是yml文件里密码不对或者URL里数据库名不存在。排查方法很简单先在本机用命令行或者Navicat测试能不能连上数据库不行就改配置。中文乱码大多是因为数据库表字符集不是utf8。建库时执行CREATE DATABASE food_community DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;连接URL里再加characterEncodingutf8基本就不会乱码了。如果页面还是显示乱码检查一下前端文件有没有声明charset或者接口返回时是否设置了响应编码。6.2 图片上传后访问404这个坑非常经典。本地开发时如果你把图片存到了本地路径比如D:/upload然后又用file:D:/upload/映射了资源路径访问应该是正常的。但部署到服务器后路径变成Linux下的/data/upload/配置没同步改就会404。另外注意路径结尾的斜杠addResourceLocations(file: uploadPath)这里uploadPath必须是以/结尾的目录路径。漏了末尾斜杠会导致映射拼接出错静态资源永远找不到。还有一个常见情况项目打成jar包后原工程里的upload目录不会自动打包进去所以那种把图片存到项目相对路径的做法在jar部署下非常容易出问题。直接用服务器绝对路径是最稳的。6.3 Spring Boot版本太高引发的依赖冲突我从网上看到不少同学按教程创建项目时直接选了Spring Boot 3.x然后MyBatis Plus用的还是老版本结果启动直接报ClassNotFoundException。这里给一个比较稳妥的版本组合组件版本Spring Boot2.7.14MyBatis Plus3.5.3MySQL Connector8.0.33JWTjjwt 0.11.5Hutool5.8.22实际上MyBatis Plus在Spring Boot 3下也有适配版但老版本不兼容新版本又要求JDK 17对很多本地环境还是JDK 8的同学不友好。不过现在很多环境其实已经可以用 JDK 17如果确定环境满足也可以选 Spring Boot 3 加 mybatis-plus-spring-boot3-starter。但作为毕设我始终坚持一个原则不折腾版本把精力花在业务功能的完整度上才是更划算的选择。6.4 接口一直报401或403前端的每一个请求我都设置了Authorization头吗检查一下网络请求工具看请求头里是否带上了token。多数403问题都是因为用户操作需认证的接口时token为空或者已过期。有个容易被忽略的细节是登录接口不是每次都返回新token我这边设置的是7天有效期。过期之后前端需要重新登录前端代码里遇到401响应时要有一个统一跳转处理否则用户会感觉系统坏掉了。如果后端设置了CORS跨域配置本地联调时会有个额外麻烦前端端口是8081后端端口是8080跨域请求如果没配置好浏览器控制台会报跨域错误。我在开发和部署时采用了前后端同端口方案所以本地直接访问8080CORS的坑也就绕开了。6.5 忘记管理员账号怎么办项目初始化sql里我预设了一个管理员账号admin初始密码123456。如果大家自己的项目把密码改了又忘记直接去数据库里执行一条更新语句把字段替换成BCrypt加密后的新值。最省事的方法是写一个小测试方法用BCryptPasswordEncoder加密明文密码把结果打到控制台然后用这条加密串更新数据库。不要尝试在数据库里直接改回明文因为登录时后端还会校验BCrypt密文。7. 我在这个项目上的一些个人心得做完这套系统之后最明显的一个感受是毕设项目不在于功能有多少炫酷而在于每个功能点你都能说明白它是怎么做的、为什么这么做。答辩时老师最常问的几个问题我提前准备了对应的回答“用户密码是怎么存的”——BCrypt加密不是明文存储。“点赞功能怎么防止重复点赞”——数据库唯一索引加业务层判断。“图片上传之后存在哪里如果服务器重启会丢吗”——存本地磁盘重启不会丢但要定期备份。“分页是怎么实现的”——MyBatis Plus分页插件传入页码和每页条数。“搜索的效率如何数据量大了怎么办”——当前用LIKE模糊查询后续可以加索引或用Elasticsearch扩展。这些问题都不难但如果你没有认真做过临场很容易支支吾吾。我自己的经验是把每个模块对应的表结构、接口请求、前端页面连成一条线写在笔记里答辩前自己过三遍基本就稳了。另外说句实在话网上有很多源码包买回来直接跑起来容易但想要在答辩时真正讲清楚还是要在本地一行行去读。把Controller层每个接口对应到前端每个按钮把Service层每个方法看懂这比你自己从零写一遍节省时间但效果并不差。部署说明和演示视频我建议也自己重做一遍因为每个人的电脑环境不一样网上给的说明不一定适配你的机器你自己跑通了才能叫真正的掌握。