ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot美食社区系统开发实战:从数据库设计到部署全攻略

Spring Boot美食社区系统开发实战:从数据库设计到部署全攻略 1. 项目到底做了什么为什么值得做这个标题看着有点长拆开看其实就是一件事用 Java Spring Boot 搭一套完整的“美食分享 菜谱笔记”社区类系统。它解决的不只是“发个菜谱”这么简单而是把做菜的人、想学菜的人和记录做菜心得的人串在同一个平台上——有人发布菜谱有人收藏别人的拿手菜有人在自己做的时候把“翻车”和“成功”的经验写成笔记形成内容闭环。先说这套系统做完之后大概的样子用户端注册登录、浏览菜谱、按分类/关键词搜索、查看菜谱详情、点赞收藏评论、关注其他用户、写自己的做菜笔记。内容端菜谱支持多图上传、配料表、步骤描述、标签、难度、耗时等结构化字段。管理端用户管理、菜谱审核/下架、分类管理、社区内容统计。放在毕设、课程设计或者 Spring Boot 入门练手项目的语境下这个题目覆盖的知识点非常典型Spring Boot 自动配置、MyBatis-Plus 数据操作、RESTful API 设计、文件上传、JWT 登录鉴权、前后端分离联调、项目打包部署。哪怕你不是做美食的把这套逻辑换成“读书分享”“健身记录”“宠物图鉴”核心代码几乎不用动。适合谁来参考一是准备 Java 毕设、需要完整源码和文档的同学二是已经学完 Spring Boot 基础、想找一个“能把所有知识点串起来”的真实项目的人三是想快速理解“一个商用风格的社区系统从 0 到 1 到底经历哪些环节”的转行开发者。下面我会把整个项目从设计到跑通的细节都讲清楚包括数据库怎么建、接口怎么写、图片往哪存、部署踩过哪些坑。2. 整体设计思路与技术选型拆解2.1 为什么选 Spring Boot MyBatis-Plus MySQL很多人问做这种平台项目用 SSHStruts2 Spring Hibernate行不行用 Python Flask 行不行都行但放在 Java 体系里Spring Boot 几乎是最不折腾的选择。先说一个最直接的原因配置量极少。以前用 Spring MVC 要配一堆 XML数据源、事务、扫描包、视图解析器每一项都要显式声明。Spring Boot 用自动配置把这一切包在约定里你只需要在 pom.xml 里引入 starter 依赖再在 application.yml 里写数据源和几个自定义参数项目就能启动起来。对要做毕设的人来讲这意味着可以把更多时间花在业务逻辑上而不是跟配置死磕。持久层我推荐 MyBatis-Plus 而不是原生 MyBatis也不是 JPA。原因是它实现了“单表操作零 SQL”User、Recipe、Note 这种简单表的增删改查直接用 BaseMapper 提供的selectById、insert、updateById就行连 XML 都不用写。而遇到多表查询比如查“某个用户发布的菜谱关联分类名”时又能退回写自定义 SQL 的路线灵活性比 JPA 更可控。对刚接触项目开发的同学来说这能少踩一半的坑。数据库用 MySQL 5.7 或 8.0 都可以。这个项目的数据量在课设、毕设场景下一般只有几百到几千条MySQL 完全够用而且它和 Spring Boot 的配合资料最多遇到问题搜索解决方案最容易。2.2 前后端到底怎么分我见过很多毕设项目把页面做在 resources/templates 里用 Thymeleaf 渲染。这样确实简单但有个隐患面试官和答辩老师会追着问“你会前后端分离吗”。所以这个项目的推荐做法是后端独立成一个 Spring Boot 工程只提供 RESTful JSON 接口前端单独用 Vue 3 Element Plus axios 搭页面通过 HTTP 请求和后端交互。这样做的好处很现实前后端职责清晰后端只关心数据前端只关心展示。答辩时能讲出“跨域处理”“接口签名”“JWT 存哪”这些亮点。后续哪怕你想把前端换成小程序或者 App后端一套接口能复用。如果你实在不想写前端也可以用后端直接返回 JSON用 Swagger 页面演示每个接口的效果。但这个项目既然定位是“平台系统”建议还是把前端做出来哪怕是基础版演示效果会好很多。2.3 功能模块怎么拆才合理我在动手前先把整个系统拆成了下面这几个模块这个拆分直接影响到包结构和数据库设计用户模块注册、登录、个人信息修改、密码重置、粉丝关注。菜谱模块发布菜谱、编辑菜谱、菜谱列表、菜谱详情、按分类浏览、关键词搜索。笔记模块写笔记、笔记列表、笔记详情、笔记和菜谱的关联。互动模块点赞菜谱、收藏菜谱、评论菜谱、收藏列表、关注列表。后台管理模块用户封禁/解封、菜谱下架、分类管理、基础数据统计。每个模块对应controller、service、mapper三层的几个类。模块拆得清楚后面写代码、写文档、画功能结构图都会顺手很多。我强烈建议在写第一行代码之前先把这个模块清单定下来不然后期很容易改着改着就乱掉。3. 数据库设计与核心表结构数据库是整个项目的“压舱石”表设计好了后面几乎不用返工。我的建议是先用表格把你需要维护的实体列出来再逐张建表。这个系统最少需要这些表3.1 用户与权限的设计用户表不需要太复杂但要有几个必须字段字段名类型说明idbigint主键自增usernamevarchar(50)登录名唯一passwordvarchar(255)BCrypt 加密后的密码nicknamevarchar(50)昵称avatarvarchar(255)头像地址roleint0 普通用户1 管理员statusint0 正常1 禁用create_timedatetime注册时间这里有两个细节需要注意第一密码一定不能用明文。Spring Security 里自带BCryptPasswordEncoder注册时把明文密码加密后入库登录时用matches方法校验。哪怕你不想引入整个 Spring Security单独引入spring-security-crypto依赖也只用这两行代码。第二管理员不单独建表用 role 字段区分就行。毕设级别的系统没必要把用户和管理员拆成两张表拆了反而增加联查成本。3.2 菜谱表与分类表菜谱表是这个系统里字段最全的一张表建表比想象中复杂一些CREATE TABLE recipe ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT 发布人, category_id BIGINT COMMENT 分类ID, title VARCHAR(100) NOT NULL COMMENT 菜谱标题, cover_image VARCHAR(255) COMMENT 封面图, images TEXT COMMENT 多图JSON数组或逗号分隔, ingredients TEXT COMMENT 配料表推荐JSON格式, steps TEXT COMMENT 步骤列表推荐JSON格式, difficulty INT DEFAULT 2 COMMENT 1简单2中等3困难, cook_time INT DEFAULT 30 COMMENT 耗时分钟, tags VARCHAR(255) COMMENT 标签逗号分隔, description TEXT COMMENT 描述, like_count INT DEFAULT 0 COMMENT 点赞数, favorite_count INT DEFAULT 0 COMMENT 收藏数, view_count INT DEFAULT 0 COMMENT 浏览数, status INT DEFAULT 0 COMMENT 0待审核1已发布2已下架, create_time DATETIME, update_time DATETIME );分类表就简单了id、name、parent_id三级目录把“家常菜/川菜/烘焙/早餐/汤羹”这种都放进去。菜谱表里存category_id查列表时 join 分类名。很多人会纠结“配料表、步骤”这种字段存什么类型。试过就知道存成 TEXT 然后放 JSON 字符串最灵活。比如配料[{name: 番茄, amount: 2个}, {name: 鸡蛋, amount: 3个}]前端可以直接JSON.parse渲染成表格后端要想统计“最常用配料”也只需要解析 JSON 字段比拆成子表简单得多。3.3 笔记、收藏、评论与点赞笔记表可以理解为“菜谱的用户二创内容”。用户做了一道菜可以在原菜谱下面写心得CREATE TABLE note ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT 作者, recipe_id BIGINT COMMENT 关联菜谱可空, title VARCHAR(100), content TEXT COMMENT 笔记正文, images TEXT COMMENT 笔记图片, like_count INT DEFAULT 0, create_time DATETIME );收藏和点赞分别是两张关联表。这里我有一个实际踩过的坑一开始为了省事把点赞和收藏设计成 user_id recipe_id 的联合唯一索引后来发现“取消收藏”“再收藏”这种操作靠一个布尔字段去控制非常容易出并发问题。更好的做法是设计成“行为记录表”点赞一次插入一条、取消就删除通过COUNT(*)来统计数量。表结构统一为表名字段一字段二说明recipe_likeuser_idrecipe_id用户对菜谱点赞recipe_favoriteuser_idrecipe_id用户收藏菜谱note_likeuser_idnote_id用户点赞笔记user_followuser_idfollow_user_id用户关注关系评论表稍微特殊一点需要支持“楼中楼”的话加一个 parent_id回复某条评论时把父评论 ID 存进去。另外评论入库前一定要做 HTML 标签过滤。这个系统是面向所有用户开放的别人在评论里塞一段script就能让你整个页面被攻击。我后来在后端加了一个全局过滤器把所有请求参数里的、转义掉这一手必须做。4. 核心功能实现与实操细节4.1 登录注册与 JWT 鉴权登录鉴权方案我推荐直接用JWTJSON Web Token没必要用 Session。原因很简单前后端分离之后前端可能部署在另一个端口Session 跨域要靠 CORS 配置和 Cookie 属性折腾半天而 JWT 只需要前端把 token 存 localStorage 里每次请求在 header 里带上Authorization字段就行。后端实现思路是这样用户登录成功后用io.jsonwebtoken生成 token把userId、username、role放进 claim。提供一个拦截器或 HandlerInterceptor拦截除/api/auth/login、/api/auth/register之外的所有/api/**请求。拦截器里解析 token解析失败就返回 401解析成功就把 userId 塞到ThreadLocal或直接放进 request attribute供后续 Controller 使用。这里有个细节token 不要在拦截器里靠数据库查用户表来验证是否有效JWT 本身就是靠签名保证不可伪造的你只需要验证签名和过期时间就行否则每次请求都打一次数据库性能会很差。前端配合的逻辑也很简单axios 封装时写一个请求拦截器统一从 localStorage 取 token 加到 header写一个响应拦截器捕获 401 状态码后跳回登录页。这一套在很多 Vue 项目里是“标准动作”。4.2 菜谱发布与图片上传这个模块是整个系统演示时的重点也是最容易出 bug 的部分。发布菜谱的接口我建议设计成接收一个 JSON 对象而不是 multipart 表单混在一起{ title: 番茄炒蛋, categoryId: 3, coverImage: http://localhost:8080/files/xxx.jpg, images: [http://localhost:8080/files/xxx1.jpg], ingredients: [{\name\:\番茄\,\amount\:\2个\}], steps: [{\content\:\热锅倒油\},{\content\:\倒入蛋液\}], difficulty: 1, cookTime: 10 }图片上传是独立的另一个接口/api/upload前端先把图片传上去拿到 URL再提交菜谱表单。这个顺序很重要——先传图、后提交表单用户就算中途关闭页面最多产生一张无用图片不会把整个表单搞坏。图片上传的保存位置我推荐存在本地磁盘而不是数据库 BLOB 字段。操作步骤是在配置里指定一个upload.path比如D:/kitchen/files/。接收MultipartFile用UUID重新生成文件名避免中文名和重复名问题。通过一个WebMvcConfigurer把这个目录映射成静态资源路径/files/**前端直接用 URL 访问。对应配置代码大概长这样Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceHandler(file: uploadPath); }4.3 列表分页与搜索排序菜谱列表页如果不用分页数据一多页面就会卡。MyBatis-Plus 自带分页插件配置很简单Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后 Service 层里直接PageRecipe page recipeMapper.selectPage( new Page(current, size), new LambdaQueryWrapperRecipe() .eq(Recipe::getStatus, 1) .like(StringUtils.hasText(keyword), Recipe::getTitle, keyword) .eq(categoryId ! null, Recipe::getCategoryId, categoryId) .orderByDesc(Recipe::getCreateTime) );这段代码里有几个实用技巧like方法的第一个条件放StringUtils.hasText(keyword)keyword 为空时自动跳过这个条件。排序用orderByDesc新发布的菜谱排在前面。“热门排序”可以改成orderByDesc(Recipe::getViewCount)这就是 tab 切换的事后端只多传一个 sort 参数。搜索这一块毕设级别用LIKE %keyword%就够了。在 5 张表、几千条数据的情况下这个模式性能没有压力。如果硬要追求大数据量下的搜索可以接 Elasticsearch但那是给自己找事面试官问起来你反而不好自圆其说。4.4 笔记与关注的业务联动笔记模块是我觉得整个项目里最能体现“平台感”的地方。普通美食网站只有“菜谱”这套系统多了一个“笔记”概念后用户的参与感会明显不一样看完菜谱自己做过一次就可以记录自己的心得和改进方案。笔记和菜谱的关联我做成“可选关联”——用户写笔记时可以不关联菜谱单纯分享今日餐桌也可以搜索并关联某个菜谱。关联之后菜谱详情页能展示“相关笔记”笔记详情页能反链“原菜谱”这就是最简单的社区内容联动。关注功能更简单user_follow 表里记录谁关注了谁。关注后在用户主页和个人笔记详情页展示笔记时增加一个“只显示我关注的人”的筛选条件wrapper.in(userId ! null, Note::getUserId, followUserIds);注意in的条件也要用userId ! null去控制否则会产生意外的 SQL 拼接。这个坑我调试了一下午才发现真的很隐蔽。5. 部署运行指南与配置说明5.1 从源码到跑起来需要准备什么环境清单如下软件版本要求备注JDK1.8 / 11推荐 11Maven3.6不用 IDE 自带也行MySQL5.7 / 8.08.0 需要改 driver 和 urlNode.js14仅前端编译需要IDEIDEA / Eclipse推荐 IDEA拿到源码后先做三件事把application.yml里的数据库账号密码改成自己的、把upload.path改成绝对路径、执行项目里带的sql脚本初始化数据库。只要这三步做完八成项目能直接启动。5.2 application.yml 里容易被忽略的配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/kitchen?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 50MB jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true里面有三个坑必须说明第一characterEncodingutf8不写的话前端传过来的中文很容易变成“???”。这个参数几乎每次新建项目都会有人漏掉。第二serverTimezoneAsia/Shanghai在 MySQL 8.0 里几乎必写不写会报“The server time zone value Öйú±ê׼ʱ¼ä”。这是时区编码的经典报错。第三map-underscore-to-camel-case开启后数据库字段create_time能自动映射到 Java 实体类的createTime不开启就会查出 null。很多人查数据查出空字段就是这个原因。5.3 完整部署流程前后端分离项目的部署一般有两种方式我推荐“生产模式打包”后端在 IDEA 里执行 Mavenpackage生成jar包。前端在项目目录执行npm run build生成dist目录。把dist文件夹整个复制到后端静态资源目录或者在 Nginx 里把/指向dist把/api反向代理到localhost:8080。如果是答辩演示其实最简单的办法是分开跑后端java -jar启动前端npm run dev启动。只要在 Vue 的vite.config.js里配一个/api的代理转发到 8080 端口就不存在跨域问题。6. 运行中的常见问题与排查技巧实录6.1 项目启动就报错先看哪里启动报错是遇到最多的场景但 90% 的问题都集中在这三类端口被占用报错Port 8080 was already in use直接到命令行执行netstat -ano | findstr 8080找到 PID任务管理器结束进程或者把server.port改成 8081。数据库连不上报错Access denied for user rootlocalhost先执行mysql -u root -p验证账号密码是不是能登录很多是 IDEA 数据库密码没同步到application.yml。驱动类找不到如果用了 MySQL 8.0 却看到ClassNotFound: com.mysql.jdbc.Driver就是 pom 里引入的老版本驱动。8.0 的 driver-class-name 是com.mysql.cj.jdbc.Driver版本号也尽量保持在 8.0.x。6.2 中文乱码与 POST 请求乱码浏览器页面显示正常、但插入数据库变问号和页面直接显示乱码是两种完全不同的场景。页面显示乱码大多是后端返回 JSON 时编码不对在配置里加server: servlet: encoding: charset: UTF-8 enabled: true force: true插入数据库变问号是数据库连接 url 缺少characterEncodingutf8上面已经说过重新看一眼连接串即可。另外建表时如果用的是 navicat 图形化工具建表要确认表的字符集是utf8mb4我之前吃过一次亏建表时默认选了latin1中文全变“???”。6.3 图片上传之后显示不出来图片上传成功但浏览器访问图片地址 404排查顺序应该是看数据库里的图片 URL 存的是相对路径还是完整域名。存/files/xxx.jpg就能访问存D:/kitchen/files/xxx.jpg这种本地绝对路径前端根本访问不到。确认addResourceHandlers有没有生效注意这个配置类要能被 Spring 扫描到最好放在启动类同一级包下。确认上传目录真的有文件。有时候 IDEA 的当前工作目录和你配置的绝对路径不一致导致文件写到了意想不到的地方。6.4 常见问题速查表症状原因解决方案前端跨域报错后端没允许跨域写 CorsFilter 全局配置允许前端地址登录后访问接口一直 401JWT token 没传或过期检查 axios 拦截器有没有带 header分页查询结果 total 为 0没配置分页插件、传参不对检查 MybatisPlusConfig 和 current/page 参数导入 sql 报语法错MySQL 版本和 sql 不适配用 5.7/8.0 对应版本的 sql 重导点赞数不变业务逻辑里只查缓存没落库改为每次操作直接 update 计数器还有一个很值得提的坑后端接口改了但前端页面没变化多半是浏览器缓存或者前端调的是 mock 接口、根本没有指向真实后端。排查时打开浏览器开发者工具的 Network 面板看一眼请求 URL 是不是http://localhost:8080/api/xxx就能快速定位。7. 项目做完之后的一点复盘这套系统我从建表到部署上线前后花了两周。不算快但每一步都走得很稳。真正做完之后我的体会是这类社区分享类系统的技术难点从来不在某个单独功能上而在于把用户、内容、互动这三层数据怎么有机地串联在一起。如果你想在这个基础上继续加亮点有几个方向成本低、收益高接入 Redis 做菜谱详情页缓存顺便用 Redis 的zset做热门菜谱排行榜。用 Elasticsearch 替换菜谱搜索能讲“倒排索引”和“分词”这两个面试高频点。增加一个简单的推荐逻辑比如“根据你收藏的分类推荐同类菜谱”哪怕用最朴素的 SQL 也能讲清楚推荐思路。最后再分享一个小技巧拿到这套源码后不要急着跑起来先打开数据库设计文档、对照着表结构看一遍然后自己在纸上画一遍模块之间的关系图等你想清楚“为什么餐桌上的每道菜在系统里都有一条数据链路”之后再开始改代码。这个习惯比你多背十道八股文都有用。
返回列表