恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

SpringBoot+Vue3美食互动平台开发实战:从数据库设计到部署避坑

  • 首页
  • 资讯中心
  • /
  • SpringBoot+Vue3美食互动平台开发实战:从数据库设计到部署避坑

相关资讯

实战型网安赛事与传统CTF有何不同?破阵阁公开赛参赛全记录 2026/10/13 10:46:22
Unity跑酷游戏开发:状态机+对象池+动态难度实战 2026/10/13 10:46:22
数据结构三要素与链表12个基础操作:从理论到C语言实战 2026/10/13 10:41:21

最新资讯

TextBlob英文情感分析实战:从快速原型到生产环境的避坑指南
定积分实战指南:可积性判断、牛顿-莱布尼茨边界与数值积分选型
MySQL教学源码包执行指南:例题案例实训实战四层解析
EmguCV单目相机标定与畸变校正实战:从棋盘格到参数应用
LVS TUN隧道模式详解:原理、配置与跨机房负载均衡实战
本地自动化工作流实战:一个人如何跑通工作室

今日推荐

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析
CAN总线错误处理机制深度解析:从位错误到Bus Off的硬件级调试

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

SpringBoot+Vue3美食互动平台开发实战:从数据库设计到部署避坑

发布时间:2026/10/13 10:46:22
SpringBoot+Vue3美食互动平台开发实战:从数据库设计到部署避坑 作为一个写过不少前后端分离项目的开发者看到这个标题的第一反应是——美食烹饪互动平台这个选题其实挺有意思的。它不像电商、后台管理系统那样烂大街功能上又天然带社交属性和内容属性菜谱发布、食材分类、收藏点赞、评论互动一套做下来几乎覆盖了主流业务系统百分之八十的通用能力。拿它练手或者作为毕业设计、简历项目性价比非常高。这篇文章我会把整个项目从技术选型、数据库设计、前后端核心实现到部署联调过程中容易踩的坑完整梳理一遍。内容偏实战提到的代码片段和表结构都是可以直接拿去用的程度。如果你是正在学SpringBoot和Vue3想找个完整项目练手或者准备做一个美食类内容平台的开发者这篇应该能帮你省下不少自己摸索的时间。1. 项目整体拆解美食互动平台要解决什么问题开始写代码之前先把需求想明白。美食烹饪互动平台核心是内容 互动四个字。用户来这个平台干什么第一找菜谱——今天想做个红烧肉搜一下看步骤、看食材、看成品图。第二学烹饪——跟着步骤做遇到问题留言问作者。第三分享自己的作品——做了拿手菜发出来晒一晒收获点赞和评论。第四沉淀关系——关注几个做菜合口味的作者之后定期看他们更新。这四个核心场景对应到系统功能上至少需要这么几个模块用户模块注册、登录、个人信息维护、头像上传。这块是所有业务系统的基础。菜谱模块菜谱的发布、编辑、删除、详情展示、分类筛选、关键词搜索。这是平台的内容核心。互动模块收藏、点赞、评论、关注。这是平台的社交血液也是和普通CMS系统最大的区别。辅助模块食材分类、菜系标签、热门排行榜、个人主页等。这些细节决定产品体验是否完整。从技术角度再看这套系统天然适合前后端分离架构。内容展示类页面需要灵活的前端交互比如菜谱步骤的逐步展开、图片懒加载、搜索结果实时过滤后台接口则要稳定、清晰、可复用一套API同时支撑Web端和将来的移动端。SpringBoot Vue3 MyBatis MySQL这套组合在这个场景下既不过度设计又足够撑起一个五脏俱全的完整系统。我见过不少人一上来就纠结微服务、分布式、Redis缓存结果连基本的菜谱CRUD都没写利索。这个项目正确的打开方式是先把单体架构做扎实接口设计规范、表结构合理、代码分层清晰有余力再考虑性能优化和架构升级。对一个互动平台的第一版来说单体应用完全够用跑起来简单出了问题也好排查。1.1 核心功能模块划分把需求转化成功能模块我习惯画一张思维导图式的脑图列出来所有页面和接口再做减法。这个项目的页面级模块大致是这样首页菜谱推荐列表、热门榜单、分类入口。数据来源是后台按浏览量、收藏量、时间倒序等维度聚合查询。菜谱列表页按分类筛选、按关键词搜索、分页加载。菜谱详情页基本信息、食材清单、步骤列表、作者信息、评论列表、收藏/点赞按钮。发布/编辑菜谱页表单 图片上传 步骤动态增删。个人中心我的菜谱、我的收藏、我的关注、粉丝列表、编辑个人资料。登录/注册页账号密码 Token鉴权。对应的后端接口大致是用户相关POST /api/auth/register、POST /api/auth/login、GET /api/user/info、PUT /api/user/info、POST /api/user/avatar —— 注册登录和资料维护。菜谱相关GET /api/recipes带分页和筛选参数、GET /api/recipes/{id}、POST /api/recipes、PUT /api/recipes/{id}、DELETE /api/recipes/{id} —— 菜谱CRUD。互动相关POST /api/recipes/{id}/like、POST /api/recipes/{id}/favorite、POST /api/recipes/{id}/comment、GET /api/recipes/{id}/comments、POST /api/user/follow —— 点赞、收藏、评论、关注。统计相关GET /api/rank/hot、GET /api/user/stats —— 排行榜和个人数据统计。这样列完整个系统的轮廓就出来了。后面所有开发和测试工作本质上就是在把这些接口一个个填实。1.2 用户角色与权限边界一个内容平台用户角色至少分成两种普通用户和内容创作者再往上就是后台管理员。普通用户可以浏览、收藏、评论、点赞可以编辑自己发布的内容。创作者是普通用户的一种自然升级形态——只要发布了菜谱就是创作者不需要额外申请。管理员则可以下架违规菜谱、删除不当评论、封禁用户。在实现上直接用JWTJSON Web Token做登录态管理最省事。用户登录成功后后端签发一个Token里面包含用户ID和角色标识前端每次请求把Token放在请求头里。后端通过拦截器解析Token拿到当前用户信息再判断是否有权限执行某操作。权限控制的关键点在于只有菜谱的作者才能修改和删除自己的菜谱。这个判断不能用前端传的参数来信任必须在后端从Token里拿用户ID再和菜谱表里的author_id比对。我见过有人图省事让前端把当前用户ID作为参数传给后端做校验结果随便伪造个ID就能删别人的内容这是高危漏洞务必注意。1.3 一个完整业务链路的设计思路拿用户发布一道菜谱并收到评论这个最核心的链路来说完整的数据流是这样的用户在前端填写菜谱标题、封面图、食材清单、步骤描述点击发布 → 前端把表单数据文本字段 图片URL数组组装成JSONPOST到后端接口 → 后端校验Token和参数把菜谱基本信息插入主表食材和步骤分别插入子表 → 菜谱出现在列表页和分类页 → 其他用户打开详情页看到完整内容 → 看完之后觉得不错提交评论 → 后端把评论写入评论表同时给菜谱作者推送一条站内消息如果做了通知功能→ 作者通过个人中心看到新评论。这条链路包含了多表写入、权限校验、前端富交互、关联查询等几乎所有核心知识点。项目开发过程中每做完一个这样的链路整个系统的完成度就往前推进一截。所以我建议的推进顺序是先做注册登录和菜谱CRUD把主链路跑通再做收藏点赞评论最后做排行榜、个人统计这类锦上添花的功能。2. 技术选型解析这套组合背后的逻辑技术选型没有绝对的对错只有合不合适。SpringBoot Vue3 MyBatis MySQL这个组合之所以常见是因为每一层都有不可替代的优势而且它们之间的配合非常顺滑。2.1 后端SpringBoot约定大于配置的生态红利SpringBoot最爽的一点是自动配置。建一个Maven项目引入spring-boot-starter-web、spring-boot-starter-validation这些依赖一个启动类就能把Web服务跑起来。对于开发者来说不需要去搞一堆XML配置注解就搞定了大部分事情。具体到这个项目SpringBoot提供的几个关键能力帮了大忙Spring MVC的RESTful接口支持。RestController注解直接暴露JSON接口RequestBody自动把请求体绑定为DTO对象参数校验有Valid这套注解写接口的效率比手写Servlet高太多。依赖注入容器。Service层、Mapper层全部交给容器管理用Autowired或构造器注入就行代码之间没有硬编码的new后期测试替换Mock对象也方便。统一的异常处理机制。通过RestControllerAdvice可以定义一个全局异常处理器业务异常、参数校验异常、系统异常分别返回不同的JSON结构。前端只需要约定好code、message、data三段式结构就能统一处理各种错误提示。版本选择上国内大多数教程用的是SpringBoot 2.7.x它基于Java 8兼容性最好。如果你用的是Java 17及以上直接用3.x版本体验更好但要注意3.x对Java 8不友好且部分第三方组件的兼容版本要确认。我建议新手直接2.7.x稳。2.2 前端Vue3组合式API让业务逻辑更好组织Vue3最核心的变化是组合式APIComposition API。用选项式API写一个小页面还行一旦组件里塞进数据请求、交互逻辑、状态管理代码就会被拆散到data、methods、computed、watch等各个部分改一个功能要来回跳。组合式API允许按功能点把逻辑聚在一起可读性和可维护性好很多。在美食烹饪互动平台的前端里几个典型场景能明显体现出Vue3的优势菜谱表单的动态字段。食材清单和制作步骤都是可以增删的数组在script setup里用reactive定义一个数组渲染层用v-for循环渲染输入框加一行删一行都只操作数组实时响应。这在选项式API里写法会更绕。组合式封装请求逻辑。把请求菜谱列表、加载更多、搜索这些逻辑抽成一个useRecipeList组合函数多个页面复用的时候直接调用函数拿到响应式数据和方法不用复制粘贴代码。状态管理Pinia。相比VuexPinia的API简洁得多去掉了mutations那层概念直接改state。用户的登录状态、个人信息这种全局数据放Pinia里任何组件都能读写。构建工具这里直接用Vite。它在开发时的冷启动速度和热更新速度比Webpack快了一个量级配置也简单创建项目一条命令搞定。2.3 MyBatis灵活SQL和代码可读性的平衡选MyBatis而不是JPA核心原因有两个。第一这个项目的查询逻辑不复杂但组合条件很多。菜谱列表页可能同时按分类、关键词、排序方式筛选。如果用了JPA或者Spring Data这种多条件动态查询要写Specification之类的东西语法别扭。MyBatis里一个动态SQL标签就解决了清晰直观。第二团队里大多数人更熟悉SQL写原生SQL可以精确控制查询语句方便针对具体业务做优化。比如热门排行榜要按收藏数排序同时关联用户昵称一个JOIN加GROUP BY搞定SQL里一目了然。不过原生MyBatis也有痛点最典型的是写简单的单表CRUD时XML里的SQL模板很啰嗦。所以实际项目里我更推荐引入MyBatis-Plus。它保留了MyBatis的全部原生能力又提供了BaseMapper这样的通用Mapper接口单表增删改查不需要写SQL直接用内置方法就行。复杂的多表关联和动态查询再自己写XML。两者结合效率和灵活性兼得。2.4 MySQL内容型业务系统的可靠底座MySQL属于那种不一定出彩但永远不会掉链子的选择。美食互动平台的并发量在早期阶段通常不高一台普通的MySQL实例完全撑得住。用InnoDB引擎支持事务、外键和行级锁菜谱发布这种涉及多表写入的操作用事务保证数据一致性出现异常直接回滚。字符集强烈建议建库时就定成utf8mb4不然用户昵称带个表情符号存进去就是乱码这种问题排查起来很让人头疼。排序规则用utf8mb4_general_ci就行不需要纠结。表结构设计上这个项目大概需要这么几张核心表后面我会展开讲用户表、分类表、菜谱表、菜谱食材表、菜谱步骤表、评论表、点赞表、收藏表、关注关系表。九张表互相通过外键逻辑关联实际开发中我一般不在数据库层面配物理外键而是靠应用层逻辑维护后面会说明原因构成了整个系统的数据骨架。3. 数据库设计九张核心表的结构与思路数据库设计直接决定后续开发效率。同一个需求表结构设计得好查询接口写得轻松扩展也方便设计不好写SQL的时候处处别扭甚至要推翻重来。3.1 用户与关注关系设计用户表是系统的基础字段如下CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(BCrypt加密), nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, bio varchar(255) DEFAULT NULL COMMENT 个人简介, role tinyint NOT NULL DEFAULT 1 COMMENT 角色 1-普通用户 2-管理员, status tinyint NOT NULL DEFAULT 1 COMMENT 状态 1-正常 0-封禁, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码字段注意两点。第一长度至少100因为BCrypt加密后的密码串比较长varchar(50)存不下。第二不要用MD5这种可逆性差的简单哈希BCrypt自带盐值抗暴力破解能力强得多。Spring Security里自带BCryptPasswordEncoder单独引入spring-security-crypto包也可以直接用。关注关系是典型的多对多关系。设计一张中间表记录谁关注了谁CREATE TABLE user_follow ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 关注者ID, follow_user_id bigint NOT NULL COMMENT 被关注者ID, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_follow (user_id,follow_user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT关注关系表;唯一约束放在(user_id, follow_user_id)上有效防止重复关注。查询某个用户关注了多少人、有多少粉丝分别对user_id和follow_user_id做COUNT即可。3.2 菜谱内容的多表拆分菜谱表是平台内容的核心。由于食材清单和制作步骤都是可变长度的列表红烧肉可能有8个步骤煮泡面可能只有2步把它们单独拆成子表避免在主表里塞逗号分隔字符串这是设计上的关键决策。菜谱主表CREATE TABLE recipe ( id bigint NOT NULL AUTO_INCREMENT, author_id bigint NOT NULL COMMENT 作者ID, category_id bigint DEFAULT NULL COMMENT 分类ID, title varchar(100) NOT NULL COMMENT 菜谱标题, cover_image varchar(255) DEFAULT NULL COMMENT 封面图URL, description varchar(500) DEFAULT NULL COMMENT 简介, difficulty tinyint DEFAULT 1 COMMENT 难度 1-简单 2-中等 3-困难, cook_time int DEFAULT NULL COMMENT 烹饪时长(分钟), view_count int NOT NULL DEFAULT 0 COMMENT 浏览量, like_count int NOT NULL DEFAULT 0 COMMENT 点赞数, favorite_count int NOT NULL DEFAULT 0 COMMENT 收藏数, status tinyint NOT NULL DEFAULT 1 COMMENT 状态 1-已发布 0-草稿, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_author_id (author_id), KEY idx_category_id (category_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜谱表;like_count和favorite_count这类统计字段和点赞表的数据是冗余的。查询详情时直接读这个字段省去每次实时COUNT的开销点赞和收藏操作发生时在事务里同时更新主表和明细表。这样设计在并发量适中的场景下完全够用也能保证列表页排序的响应速度。食材和步骤子表CREATE TABLE recipe_ingredient ( id bigint NOT NULL AUTO_INCREMENT, recipe_id bigint NOT NULL, name varchar(100) NOT NULL COMMENT 食材名称, amount varchar(50) DEFAULT NULL COMMENT 用量如500克, sort_order int NOT NULL DEFAULT 0 COMMENT 排序, PRIMARY KEY (id), KEY idx_recipe_id (recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT食材表; CREATE TABLE recipe_step ( id bigint NOT NULL AUTO_INCREMENT, recipe_id bigint NOT NULL, content text NOT NULL COMMENT 步骤描述, image varchar(255) DEFAULT NULL COMMENT 步骤图URL, sort_order int NOT NULL DEFAULT 0 COMMENT 步骤序号, PRIMARY KEY (id), KEY idx_recipe_id (recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT步骤表;查询详情时一条SQL查主表两条SQL查子表组装后返回给前端逻辑清晰。发布菜谱时在同一个事务里分别插入主表、清空子表、批量插入子表数据一致性有保障。分类表比较简单CREATE TABLE category ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 分类名称, sort_order int NOT NULL DEFAULT 0 COMMENT 排序, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT分类表;预先插入常用数据家常菜、川菜、粤菜、烘焙甜品、汤羹粥、小吃夜宵等前端首页的分类入口直接从这里读。3.3 互动数据模型点赞、收藏、评论互动相关的三张表设计逻辑是类似的。点赞表和收藏表本质是用户和菜谱的关联关系唯一区别是业务语义不同。CREATE TABLE recipe_like ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, recipe_id bigint NOT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_recipe (user_id,recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT点赞表; CREATE TABLE recipe_favorite ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, recipe_id bigint NOT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_recipe (user_id,recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表; CREATE TABLE recipe_comment ( id bigint NOT NULL AUTO_INCREMENT, recipe_id bigint NOT NULL, user_id bigint NOT NULL, content varchar(500) NOT NULL COMMENT 评论内容, parent_id bigint DEFAULT NULL COMMENT 父评论ID支持楼中楼, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_recipe_id (recipe_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;评论表里加parent_id可以支持回复功能同一菜谱下的评论按时间倒序查出之后前端拼成树形结构展示。如果不想做二级回复保留parent_id字段但不实现前端交互也完全可以白名单里留出扩展空间。3.4 索引设计与查询性能考量几张核心表建完索引策略有几个关键点。第一外键关联字段必须建索引比如recipe表的category_id、comment表的recipe_id否则关联查询时会全表扫描数据量一上来就出问题。第二排序字段如果经常用也考虑建索引菜谱列表按create_time排序时idx_create_time就能生效。第三unique索引除了保证数据唯一性对按用户名登录这种高频查询也有很大性能提升。为什么不在数据库层面配物理外键这是实战中很多人的习惯。物理外键会带来几个问题删数据时要考虑级联操作容易误删关联数据双表写入时表锁和行锁的范围更大高并发场景下性能变差项目迭代中改表结构也更麻烦。实际做法是应用层保证数据完整性——删除菜谱时在Service层写SQL同时删除食材、步骤、评论和互动记录事务提交后数据依然是干净的。4. 后端SpringBoot核心实现从分层架构到关键业务逻辑工程结构清晰是项目长期维护的基石。这个项目我把后端按标准分层来组织每层只负责自己的事。4.1 项目分层与命名规范com.example.foodplatform ├── controller # 接口层接收请求参数返回JSON │ ├── AuthController.java │ ├── RecipeController.java │ ├── UserController.java │ └── CommentController.java ├── service # 业务层核心逻辑全部在这里 │ ├── impl │ │ ├── RecipeServiceImpl.java │ │ └── ... │ └── RecipeService.java ├── mapper # 数据访问层继承BaseMapper │ ├── RecipeMapper.java │ └── ... ├── entity # 与数据库表对应的实体类 ├── dto # 数据传输对象接收前端参数 ├── vo # 视图对象返回给前端的数据 ├── config # 配置类如CORS、拦截器 ├── common # 通用类如Result、异常处理 └── utils # 工具类如JWT工具Controller层要薄只做参数接收和数据返回不写业务代码。Service层负责具体逻辑事务注解加在Service层方法上。Mapper层只写SQL和数据映射。这样分层的核心好处是出问题的时候定位快接口报错先看Controller参数业务逻辑错了进Service看SQL慢就排查Mapper。三层之间用DTO和VO隔开前端传参的字段结构和数据库表结构不一致时不会互相污染。DTO和VO必须分开。举个例子发布菜谱时前端传的JSON里有title、coverImage、description、ingredients数组、steps数组这些字段在菜谱表、食材表、步骤表里各存一半。后端不能直接把前端JSON绑定到entity上而是先绑定DTO然后在Service层拆解、组装、分别入库。返回给前端的数据也可以裁剪掉敏感字段比如用户密码、创建时间等。4.2 统一返回结构与全局异常处理前后端分离项目接口返回格式必须统一。我用的结构是这个{ code: 200, message: 操作成功, data: { } }code为200表示成功其他业务码表示各类失败。对应的Java类public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message 操作成功; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }全局异常处理用RestControllerAdvice实现。哪些场景需要自定义异常比如菜谱不存在404、没有权限修改403、用户已存在400、参数校验失败400。把这些都定义成RuntimeException的子类在Service层抛出来由全局异常处理器统一捕获并转换成Result返回。这样Controller里不需要写try-catch代码干净得多。RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public ResultVoid handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } ExceptionHandler(MethodArgumentNotValidException.class) public ResultVoid handleValidException(MethodArgumentNotValidException e) { String msg e.getBindingResult().getFieldError().getDefaultMessage(); return Result.error(400, msg); } ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { return Result.error(500, 系统异常请稍后重试); } }4.3 注册登录与JWT鉴权实现登录注册这块核心实现分三步。第一步注册接口接收用户名、密码、昵称校验用户名是否已存在密码用BCrypt加密之后写入用户表。第二步登录接口校验用户名密码成功后用JWT工具生成Token返回给前端。第三步前端后续所有请求在请求头里带Authorization: Bearer {token}后端拦截器统一解析和校验。JWT工具类的关键代码Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; // 单位毫秒 public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(userId.toString()) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Long getUserId(String token) { Claims claims Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }拦截器里的逻辑也比较简单解析Token拿到用户ID放到ThreadLocal里后续Controller和Service通过上下文工具类获取当前登录用户。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { // 解析token成功后放行失败返回401 } return true; } }JWT的secret一定要放到配置文件里不要写死在代码里。生产环境的secret需要足够长、足够随机。Token过期时间设成7天比较合理太短用户要频繁登录太长有安全隐患。4.4 菜谱发布功能的事务处理发布菜谱是典型的写多张表场景。Service层方法上添加Transactional注解方法内按顺序执行插入菜谱主表拿到ID、批量插入食材列表、批量插入步骤列表。其中一个插入失败整个事务回滚不会出现菜谱有主记录但没食材这种脏数据。Transactional(rollbackFor Exception.class) public Long createRecipe(RecipeCreateDTO dto) { // 1. 组装主表实体 Recipe recipe new Recipe(); BeanUtils.copyProperties(dto, recipe); recipe.setAuthorId(currentUserId()); recipeMapper.insert(recipe); // 2. 插入食材列表 if (dto.getIngredients() ! null) { for (IngredientDTO item : dto.getIngredients()) { RecipeIngredient ingredient new RecipeIngredient(); ingredient.setRecipeId(recipe.getId()); ingredient.setName(item.getName()); ingredient.setAmount(item.getAmount()); ingredient.setSortOrder(item.getSortOrder()); recipeIngredientMapper.insert(ingredient); } } // 3. 插入步骤列表代码结构同上 // ... return recipe.getId(); }MyBatis-Plus的insert方法默认为每条SQL单独执行。批量插入性能在数据量不大时没问题但如果一步插几百条食材建议用自定义XML里的foreach批量插入一条SQL搞定速度提升明显。点赞和收藏功能要防止重复操作。前端按钮点击后后端先查唯一索引user_id recipe_id存在就提示已点赞不存在才插入并更新计数。这里的并发检查靠数据库的唯一索引兜底即使两个人同时请求也只有一个能插入成功另一个会报唯一约束异常。4.5 菜谱列表的多条件动态查询菜谱列表页最常用的查询是按分类筛选、按关键词模糊搜索标题、按时间或热度排序、分页。MyBatis的XML动态SQL是干这个的利器。select idselectRecipePage resultTypecom.example.foodplatform.vo.RecipeListVO SELECT r.id, r.title, r.cover_image AS coverImage, r.difficulty, r.cook_time AS cookTime, r.view_count AS viewCount, r.like_count AS likeCount, r.favorite_count AS favoriteCount, r.create_time AS createTime, u.nickname AS authorName, u.avatar AS authorAvatar FROM recipe r LEFT JOIN user u ON r.author_id u.id where if testcategoryId ! null AND r.category_id #{categoryId} /if if testkeyword ! null and keyword ! AND r.title LIKE CONCAT(%, #{keyword}, %) /if AND r.status 1 /where ORDER BY choose when testsort hotr.view_count DESC/when otherwiser.create_time DESC/otherwise /choose LIMIT #{offset}, #{size} /select标签会自动去掉子句前多余的AND这是常用技巧。排序字段用 做了白名单限制避免用户通过参数直接拼接SQL注入。分页参数offset和size由前端传入。这里有个经验之谈LIKE模糊查询在数据量大的情况下性能很差但一个学习项目或者初期平台几百上千条菜谱完全不用担心正常写就行。真要优化要么上ES要么用全文索引那是后面的事现在不要过度设计。4.6 图片上传本地存储方案菜谱封面、步骤图、用户头像本质都是图片上传。云存储要申请账号配密钥学习项目直接本地存储最省事后续要迁移也很容易因为数据库里存的是访问URL换成云存储URL前缀就行。后端实现方案配置一个虚拟路径映射实际磁盘路径。在application.yml里加配置file: upload-dir: /home/app/resources/images/ access-path: /images/**对应的配置类Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceHandler(file: uploadDir); } }上传接口接收MultipartFile用UUID生成新文件名避免重名覆盖然后保存到磁盘。保存成功后返回访问路径前端直接用这个路径拼在img标签的src上。文件类型校验很有必要。只允许jpg、png、jpeg、webp等格式限制文件大小5MB以内Spring配置文件里也可以配max-file-size。否则用户传个MP4进来服务端磁盘直接被打满。5. 前端Vue3实现要点从工程搭建到交互细节前端是整个系统给人的第一印象画面好看与否、操作是否顺滑直接决定用户愿不愿意用。Vue3 Vite Pinia Element Plus或Naive UI这套组合能让开发效率翻倍。5.1 工程初始化与目录结构创建项目直接用它npm create vitelatest food-web -- --template vue安装依赖npm install vue-router4 pinia axios element-plus element-plus/icons-vue目录结构src ├── api # 接口请求封装 │ ├── recipe.js │ ├── auth.js │ └── user.js ├── router # 路由配置 ├── stores # Pinia状态 │ └── user.js ├── views # 页面组件 │ ├── HomeView.vue │ ├── RecipeDetailView.vue │ └── RecipeEditView.vue ├── components # 公共组件 │ ├── RecipeCard.vue │ ├── CommentList.vue │ └── RecipeStepEditor.vue ├── utils # 工具函数 │ └── request.js └── App.vueaxios请求统一封装是一个必须做的事。在utils/request.js里配置baseURL、请求拦截器自动附加Token响应拦截器统一处理错误码。这样每个业务接口只需要写请求方法本身不重复处理token和错误提示这些公共逻辑。5.2 接口请求封装示例核心页面的接口封装以菜谱模块为例import request from ../utils/request // 分页查询菜谱列表 export function getRecipes(params) { return request.get(/api/recipes, { params }) } // 查询菜谱详情 export function getRecipeDetail(id) { return request.get(/api/recipes/${id}) } // 发布菜谱 export function createRecipe(data) { return request.post(/api/recipes, data) } // 点赞 export function likeRecipe(id) { return request.post(/api/recipes/${id}/like) }这些函数在组件里被调用后拿到的直接就是data数据因为响应拦截器已经把Result包装解开了。5.3 关键页面的组件化设计首页是列表流上面是分类标签下面是菜谱卡片列表滚动到底部自动加载下一页。菜谱卡片单独抽成RecipeCard组件接收一个recipe对象渲染封面图、标题、作者昵称、点赞数等信息。这个组件在首页、搜索页、个人中心都会用到抽出来一次性写好吃遍所有场景。菜谱详情页是交互最集中的地方。顶部是封面图和标题简介接着是食材清单然后是步骤列表每个步骤前有编号可以配图。页面底部是评论区和点赞收藏按钮。这个页面需要同时展示菜谱信息、作者信息、互动状态当前用户是否已点赞、已收藏这些数据可以从详情接口一次性返回。发布菜谱页用动态表单。食材和步骤都是可增删的数组每个数组项渲染一个输入区域。步骤项之间用分隔线隔开点击添加步骤按钮往数组里push一个空对象渲染层自动多出一行输入框。为了更好的体验步骤编辑器可以单独拆成RecipeStepEditor组件内部管理步骤列表向外emit变更事件父组件通过v-model双向绑定。5.4 路由守卫与状态持久化用户登录状态用Pinia管理但页面刷新后Pinia里的数据会清空。所以登录成功后不仅要存Token到localStorage还要把用户基本信息也存一份。刷新页面时在路由守卫或者初始化逻辑里先从localStorage恢复用户状态再根据Token请求后端更新用户资料。路由守卫主要控制权限。router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.isLogin) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })发布菜谱页、个人中心这些需要登录的页面在路由meta里标注requiresAuth: true。用户没登录点了直接跳转登录页登录成功后带着redirect参数跳回原页面体验很丝滑。5.5 图片懒加载与列表性能菜谱列表页如果一口气渲染几十张封面图浏览器请求压力会很大尤其是网络慢的时候。Element Plus自带el-image组件内置懒加载属性只需el-image :srcrecipe.coverImage lazy fitcover/el-image列表滚动加载实现也不复杂。监听window的scroll事件判断滚动位置是否接近页面底部接近时触发下一页加载加载完成后拼接新数据到列表里。防抖要加上不然滚动一次触发十几次请求。分页加载时要小心v-for的key值。菜谱ID是唯一的直接拿它当key避免列表重渲染时出现状态错乱。删除某个菜谱后也要从列表中过滤掉对应条目而不是重新拉全量列表。6. 联调与部署过程中的常见问题排查实录前后端分离项目开发模式没问题不代表部署时一帆风顺。这里我把自己实际踩过的坑整理成问题排查记录每一个都费过不少排查时间写出来帮大家避雷。6.1 跨域配置为什么前端请求到不了后端最经典的坑。开发阶段前端跑在localhost:5173Vite默认端口后端跑在localhost:8080前端发请求浏览器的同源策略直接把请求拦截了控制台报CORS错误。解决办法是在后端加跨域配置允许指定来源访问。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOrigins改成allowedOriginPatterns是因为allowCredentials(true)时allowedOrigins不能是通配符*这是SpringBoot的规范限制。如果前端用了带Cookie的请求这里必须配allowCredentials(true)。生产环境部署后如果前端静态资源用Nginx托管后端接口也通过Nginx反向代理前后端同域就彻底没有跨域问题了。开发阶段保留上面这段配置部署阶段用Nginx配置转发两不误。6.2 MyBatis查询结果全为null驼峰映射的坑用MyBatis时如果数据库字段是cover_image这种下划线命名Java实体属性是coverImage这种驼峰命名查询结果会全部映射为null。原因很简单MyBatis默认没有开启下划线到驼峰的自动映射。解决办法在application.yml里配置mybatis: configuration: map-underscore-to-camel-case: true如果用了MyBatis-Plus它默认是开启的所以用MyBatis-Plus时一般不遇到这个问题。自己手写XML时则要确认这条配置已经生效或者SQL里显式写别名。6.3 文件上传失败默认上传大小限制SpringMVC默认的单文件上传大小限制是1MB默认请求总大小是10MB。菜谱封面图动辄两三MB发布时直接报错MultipartException。需要在配置里调整spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB前端也要同步限制Element Plus的el-upload组件配了:limit和:before-upload校验超过大小给用户清晰提示而不是等后端报错。6.4 空指针异常联查数据缺失的定位思路列表或详情页显示作者昵称时如果有个用户被删了LEFT JOIN查出来昵称是null页面不处理就直接渲染空白或报错。处理思路分三步。第一前端模板里做空值兜底要么显示默认昵称要么显示已注销用户。第二后端VO的字段允许为null但文档里声明的返回结构要说清楚。第三如果业务上不允许用户被直接删除比如有内容关联更合理的方案是不提供物理删除而是把用户status改为0封禁这样内容还在只是账号不可登录。我强烈推荐用这种方式保留关联数据。6.5 排行榜数据重复缺少分页导致的数据错乱热门排行榜按浏览量倒序如果列表是分页加载的排行榜接口的排序查询和分页一起用时要特别注意ORDER BY字段有重复值时分页结果可能出现同一行数据在不同页重复出现。比如有五条记录浏览量都是100按view_count排序后它们的相对顺序不固定MySQL每次查询顺序可能不同第一页显示了记录A第二页又显示了A。解决办法很简单排序加上次要字段比如ORDER BY view_count DESC, id DESC确保顺序完全确定。这是个非常隐蔽的问题数据量小的时候看不出来数据量一大就冒出来了。6.6 事务失效的常见原因Transactional失效的情况最典型的有三种。第一方法不是public的Spring的代理机制要求事务方法必须是public。第二同类内部方法调用比如Service里方法A调了方法BA没事务注解B有TransactionalB的事务不生效因为代理只拦了外部调用A的方法入口。第三事务注解加到了private方法上同样不起作用。还有一个容易被忽略的细节Transactional默认只回滚RuntimeException如果方法里抛了受检异常Exception的子类而非RuntimeException事务不会回滚。要么方法声明里指定rollbackFor Exception.class要么确保所有业务异常都是RuntimeException类型。上面我的代码示例里写的就是rollbackFor Exception.class这是最保险的写法。6.7 部署阶段的环境配置管理本地开发、测试、生产三个环境数据库连接、文件上传路径、JWT密钥这些配置都不一样。用SpringBoot的多环境配置特性一套代码到处跑spring: profiles: active: devapplication-dev.yml放开发配置application-prod.yml放生产配置。启动时通过--spring.profiles.activeprod参数指定环境。生产环境的数据库密码、JWT密钥用环境变量注入不要明文写在配置文件里提交到代码仓库。部署方式上最简单的方案是打jar包直接跑mvn clean package然后把target下的jar包传到服务器java -jar启动。前端构建之后把dist目录放到Nginx的html目录同时配置反向代理把/api开头请求转发到后端服务。一套流程下来一个域名就能同时服务前后端。写在最后的几点个人体会这个项目从零到完整可运行我前后大概花了两周业余时间中间踩的坑基本都写在上面了。通读一遍应该能感觉到它不是一个从教程里复制粘贴就能交差的Demo而是真正需要考虑需求、设计数据结构、处理边界情况的完整项目。做完之后你对SpringBoot的核心机制、Vue3的组合式API、多表联合查询和事务控制的理解会比看十遍文档来得扎实。最后分享一个小建议不要拘泥于标题里列出的技术栈。这个平台的服务端代码写顺了完全可以再拆出管理后台复用大部分后端接口菜谱的数据结构清晰做成App的接口也毫不违和。核心的架构思路和代码设计是通用的换皮不换骨而已。做的时候多想一步做出来的东西就能多走一步。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号