
1. 项目定位与核心价值1.1 这套美食网站究竟长什么样搞Java后端这些年带过不少新人也看过很多课程项目。一个特别普遍的现象是很多东西单独拎出来都会SpringBoot能跑Vue3能配MyBatis会写但把三个凑成一个完整的前后端分离网站系统就卡壳了——不是不知道接口怎么定就是被跨域折腾半天要么建表建得随心所欲联调时改来改去。这篇要聊的是一套实际能跑的BS架构浏览器/服务器架构美食网站系统。后端用Java SpringBoot数据访问层用MyBatis数据库是MySQL前端用Vue3 Vite Element Plus前后端之间通过RESTful接口通信身份认证走JWT。整个项目覆盖了一个完整互联网站从零到上线所需要的大部分环节数据库建模、服务端接口设计、用户注册登录、前台内容展示、后台数据管理。它做的具体事情很直白用户打开浏览器可以看到首页推荐的各种菜品按分类筛选食物点进详情页看用料和制作步骤登录后可以收藏喜欢的美食、发表评论管理员则从另一个入口登录后台维护菜品信息、管理分类和用户。看起来是个小型网站但它五脏俱全前端有路由和状态管理后端有分层架构和安全控制属于典型的“教学用得上、面试讲得出、二次开发跑得动”的完整源码项目。1.2 主角是美食但骨架是全栈分离选美食这个垂直领域是有讲究的。纯CRUD的“学生管理系统”“图书管理系统”已经烂大街了面试官一眼就能看出来是照着模板抄的。美食网站不一样它的业务逻辑虽然不复杂但功能面足够宽菜品分类有层级关系搜索有条件拼接收藏有用户关联评论有时间排序后台有图片上传。这些恰好是前后端分离项目里最常见的通用场景。更关键的是美食主题让整个开发过程不那么枯燥。你在设计数据库的时候脑子里想的是“麻辣香锅到底放哪个分类”而不是对着“用户表加个字段叫remark”发呆。项目如果连开发者自己都觉得无聊那写到一半放弃的概率就会直线上升。选一个自己真正愿意研究的东西作为载体听起来像玄学但确实是能跑完项目和烂尾项目之间最现实的区分点之一。这套系统适合三类人正在准备毕业设计的在校生想从纯前端或者老PHP/C#技术栈转Java全栈的工程师以及学完基础语法但不知道如何串起来做个完整东西的初学者。如果你已经在公司写过一两年业务代码那这套系统的技术深度可能满足不了你但它作为梳理前后端分离通用套路的参考价值依然值得翻一翻。2. 技术选型背后的关键决策2.1 SpringBoot版本选择一个容易翻车的地方做Java后端SpringBoot版本这事我吃过亏。热词里有一条“springboot版本太高”简直说到心坎里了。SpringBoot 3.0之后Java版本要求直接跳到JDK 17很多人的电脑上还装着JDK 8Maven也配的旧版本结果项目一导入全是红叉不是“不支持发行版本5”就是“程序包org.springframework.boot不存在”最后折腾半天把版本降回2.7.18就一切正常。这套美食网站源码用的是SpringBoot 2.7.x搭配JDK 8/11都没问题属于目前生态最成熟的组合。说实话如果不是有硬性的新特性需求新手项目选2.7.x是性价比最高的决定教程最多、兼容性最好、网上踩坑记录也最全。你拿3.x去做毕业设计不仅导师未必懂自己排查问题的难度也会翻倍。版本对应关系SpringBoot版本JDK要求Maven建议适合场景2.7.xJDK 8/113.6课程设计、毕业设计、中小型业务3.0.x及以后JDK 173.6想用新特性、从零开始的老项目升级你拿到源码第一步先检查自己的JDK版本。命令行输入java -version是1.8版本就放心跑2.7.x如果已经装了17那也建议为了这个项目临时装一个8的JDK切换环境而不是反过来去升级项目依赖。我见过太多人把时间浪费在“升级依赖—报错—查半天—再升级”的循环里最后项目没跑起来心态先崩了。2.2 MyBatis还是MyBatis-Plus为什么不用JPAORM框架选型是Java项目里最容易被低估的决策。JPA/Hibernate这类全自动框架写起来舒服实体类一映射表自动建了增删改查都有默认实现但它们有个致命问题复杂查询时SQL不好控而且一旦使用不当N1查询能把数据库拖垮。如果你问团队里干了七八年的老Java大多数人对JPA的态度都是“能用但不敢乱用”。MyBatis和MyBatis-Plus则相反SQL是明文写在Mapper XML里的你完全知道每一条查询在干什么。面试的时候MyBatis也是问得最细的一个话题缓存机制、动态SQL、参数映射随便一问就能看出候选人到底是背了笔记还是真的写过大半年项目。这套系统选择的是纯MyBatis。为什么不用MyBatis-Plus核心原因是教学场景下我希望把SQL的控制权攥在手里毕竟动态SQL和结果映射才是MyBatis的精髓所在。Plus虽然把单表CRUD封装到了极致甚至能根据Java实体类生成创建表的SQL语句但这种便利性容易让新手跳过SQL这一步后面一旦遇到复杂多表查询照样露馅。用MyBatis你会发现自己在写SQL的过程中真的在读数据真的在理解表结构这是被框架包裹住的时候体会不到的。2.3 Vue3 Vite Element Plus组合的正确性前端部分选Vue3其实不需要太多纠结。Vue2已经停止维护新项目再不用Vue3就是给自己挖坑。真正需要想清楚的是构建工具Vite还是Webpack。Vite的开发服务器启动速度是真的快因为它按需编译不像Webpack那样一启动就要把整个项目都打包一遍。对这个美食网站来说页面不算多Webpack那几分钟的启动时间可能感觉不明显但开发体验上的流畅感是实实在在的。Element Plus是Vue3对应的UI组件库后台管理系统那种表格加表单的界面用组件库能省掉80%的样式时间。有一点需要提醒Element Plus的组件建议按需引入不要一股脑全量注册否则最终打包体积会大上很多首屏加载速度也会受影响。很多人在项目里用全量引入图省事结果打包出来一个2MB的js文件就是没考虑这一点。Vue3的核心概念里ref和reactive是绕不开的。很多新手踩过一个坑用reactive定义了一个对象然后在某个方法里直接整个把它替换掉结果发现页面不更新。这是因为reactive返回的是代理对象你把它赋值成一个新的普通对象代理关系就断了。后来我才彻底想明白简单场景用ref就不会有这个问题但凡是对象嵌套比较深、需要整体替换的场景要么用ref包一层要么用reactive时永远通过.field xxx的方式改属性而不能整体赋值。这套系统里我用ref比较多就是为了少踩这个坑。2.4 MySQL单库在这个体量下够用了后端存储这块很多人一上来就考虑Redis缓存、Elasticsearch搜索但对一个菜品几百上千条、用户量几千级的网站来说这是典型的过度设计。MySQL单库单表在这个数据量级下性能绰绰有余查询加合适的索引毫秒级返回毫无压力。这套美食网站系统的数据库设计就是个典型的MySQL实践样本用户表、菜品分类表、菜品表、收藏表、评论表五张核心表外加一些辅助字段。线上部署到一台2核4G的云服务器上跑个几百人同时访问没有任何问题。真要等哪天用户量变成几万人再来考虑读写分离、缓存分层这些事也不迟。架构这东西永远是为当前规模和可预见的增长服务的。3. 核心功能拆解与数据库设计3.1 三个角色三种权限菜单跟着身份走美食网站系统的用户角色很清晰游客、注册用户、管理员。游客只能在首页和菜品列表页逛一逛看看详情一旦点击“收藏”或“发表评论”前端就会弹出登录框注册用户登录后除了浏览还能收藏菜品、评论、维护个人中心信息管理员登录后台位于另一个完全独立的界面。前后端分离项目里权限控制通常在两处做。前端通过路由守卫控制页面跳转比如未登录的用户访问“收藏列表”页面会强制重定向到登录页后端则通过拦截器校验JWT客户端发的请求如果没有带上有效的Token直接返回401。这种双重防线是必要且合理的——前端的限制只服务于体验后端的校验才是真正的安全防线。3.2 五张核心数据表建表SQL直接抄数据库设计是整个系统的地基。地基打歪了后面写多少代码都别扭。这套系统里我给出的建表思路非常简单直白但细节上都有讲究。-- 用户表 CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(MD5加密), nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像路径, role tinyint NOT NULL DEFAULT 1 COMMENT 角色:0-管理员 1-普通用户, status tinyint NOT NULL DEFAULT 1 COMMENT 状态:1-正常 0-禁用, create_time datetime NOT NULL COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 菜品分类表 CREATE TABLE food_category ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(50) NOT NULL COMMENT 分类名称, sort int NOT NULL DEFAULT 0 COMMENT 排序值(越小越靠前), create_time datetime NOT NULL COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品分类表; -- 菜品表 CREATE TABLE food_info ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, category_id bigint NOT NULL COMMENT 所属分类ID, name varchar(100) NOT NULL COMMENT 菜品名称, price decimal(10,2) NOT NULL COMMENT 价格, image varchar(255) DEFAULT NULL COMMENT 菜品图片路径, description text COMMENT 菜品描述, steps text COMMENT 制作步骤, status tinyint NOT NULL DEFAULT 1 COMMENT 状态:1-上架 0-下架, view_count int NOT NULL DEFAULT 0 COMMENT 浏览量, create_time datetime NOT NULL COMMENT 创建时间, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表; -- 收藏表 CREATE TABLE favorite ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, food_id bigint NOT NULL COMMENT 菜品ID, create_time datetime NOT NULL COMMENT 收藏时间, PRIMARY KEY (id), UNIQUE KEY uk_user_food (user_id,food_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表; -- 评论表 CREATE TABLE comment ( id bigint NOT NULL AUTO_INCREMENT, food_id bigint NOT NULL COMMENT 菜品ID, user_id bigint NOT NULL COMMENT 评论用户ID, content varchar(500) NOT NULL COMMENT 评论内容, create_time datetime NOT NULL COMMENT 评论时间, PRIMARY KEY (id), KEY idx_food (food_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这套表结构在数据库设计上属于最基础的三范式模型每个表之间通过ID逻辑关联。在收藏表上我特意加了一个联合唯一索引(user_id, food_id)这是为了防止同一个用户重复收藏同一道菜——如果你不建这个唯一索引代码里又忘了查重数据库层面就会漏进脏数据这个细节很容易被忽略。3.3 建表和类型选择里那些不起眼的规范价格字段用decimal(10,2)不用float或double。浮点数在MySQL里存价格会出问题比如0.1 0.2可能得到0.30000000000000004虽然平时看着没事订单金额一累计就会出现分毫误差。虽然这个美食网站不做支付价格只是展示用但这是一个应该养成的习惯——涉及钱一律定点数。菜品图片存的是路径不是二进制。图片文件落在服务器的上传目录里数据库的image字段只存一个相对路径。这样数据库表不会变得臃肿查询速度快以后把图片迁移到OSS对象存储也容易。制作步骤用text类型。为什么不拆成单独的表因为美食网站的做法展示通常是整块的文本拆了反而增加关联查询的复杂度。只有当你需要“步骤支持单独评论、单步骤点赞”这类场景时才有拆表的必要。做系统设计时一定要先问自己“这个字段未来会被单独查询吗”答案是否定的情况下尽量不要过度设计。所有表都加了create_time这是一个小习惯但极其重要。你永远不知道什么时候就需要查一条数据的创建时间去做统计或者排查问题。有些员工建表图省事不加等运营想要“这个月新增了多少菜品”的时候就傻眼了。4. 关键实现环节与技术细节4.1 后端分层架构与统一返回格式后端代码我按Controller-Service-Mapper三层来组织这是绝大多数Java后端项目的标准姿势。Controller只负责接收参数和返回结果不写业务逻辑Service层承载业务规则处理事务Mapper层只做数据库读写。这样分层的意义在于出问题时能快速定位——前端传参不对先看Controller业务逻辑算错了看ServiceSQL写错了看Mapper。接口返回值必须统一封装不要不同接口各返回各的格式。这套系统里我定义了一个通用的Result对象结构是{ code, message, data }code为200表示成功401表示未登录500表示服务端异常。所有接口返回的都是这个结构前端只用解析一种格式就行处理错误时也只用看code。后端还有两个容易被忽略但非常影响开发体验的环节全局异常处理和分页。全局异常处理用Spring的RestControllerAdvice把业务异常统一转换成Result返回这个很省事不然每个Controller里都要加try-catch代码写得想吐。分页方面列表页和后台表格都需要分页参数是pageNum和pageSize返回结构里包含total总数这样前端就知道共有多少页了。4.2 MyBatis里值得写下来的几个关键点MyBatis是这套系统的数据访问核心。映射配置里有两个重点第一全局开启驼峰命名映射数据库字段create_time能自动映射到实体类的createTime省掉一堆resultMap手写配置第二复杂的多表查询、动态SQL才需要resultMap和XML里的拼接比如菜品列表的搜索功能要按分类筛选又要按名称模糊匹配用动态SQL写就是典型的where标签加if判断。#{}和${}的区别这是MyBatis面试必考题也是实际开发里最容易出事的地方。我在写动态SQL时凡是值都走#{}预编译绝不直接拼字符串。${}只有在动态排序列名或者表名时才用而且必须保证传参可控。你在这个系统的源码里只要看到排序字段那种位置都是经过前端选项白名单校验的不会直接把用户输入拼进去。MyBatis的缓存机制也值得多说一句。一级缓存默认开启范围是SqlSession二级缓存需要手动开启范围是Mapper级别。这个美食网站的数据变化不算频繁我开了二级缓存因为菜品的浏览量大、写入少缓存命中后性能提升立竿见影。但直接后果是后台更新菜品信息时必须显式刷新缓存不然前台看到的还是旧数据。要不要开二级缓存取决于你的业务到底是读多写少还是写多读少读多写少才值得开。4.3 前端Vue3的工程化实践与状态管理Vue3项目使用Vite创建命令是npm create vitelatest选择Vue模板然后npm install安装依赖。目录结构上src/api统一放请求函数src/router放路由配置src/store放全局状态src/views放页面组件。这是目前Vue3后台管理类项目最主流的结构按这个组织找文件和加功能都方便。状态管理用的是Pinia。Vuex在Vue3时代已经算历史遗留了Pinia更轻量写法也更简单。这个系统里用户登录信息、Token、昵称头像这些全局数据都放在Pinia里。登录成功后后端返回Token前端存到localStorage同时把用户信息写进Pinia。请求拦截器里每次带Token响应拦截器里如果发现返回的code是401就清空本地存储并跳转登录页。路由守卫是权限控制的前端半边天。router.beforeEach里检查目标路由元信息meta如果meta里标了requiresAuth: true再看Pinia里有没有用户信息。没有就跳登录页这是所有后台管理系统都会用到的通用逻辑。Element Plus按需引入的操作是下载unplugin-auto-import和unplugin-vue-components这两个插件在vite.config.js里配置一下然后组件就能自动按需加载了。如果没有这一步全量引入Element Plus时打包体积会明显增加不说页面加载速度也会被拖累。4.4 前后端如何优雅地联调前后端分离项目最烦的过程就是联调。后端在localhost:8080跑着前端在localhost:5173跑着端口都不一样直接发请求就会被浏览器跨域拦截。开发环境解决跨域最优雅的办法不是让后端写个CORS配置类而是在Vite的代理配置里做转发。vite.config.js里配置server.proxy把/api开头的请求转发到http://localhost:8080这样浏览器的请求就都在同一个域名下了也不会触发跨域策略。这套系统后端接口的统一前缀是/api一部分原因就是为了配合这个代理规则前端请求写/api/food/list后端收到的也是这个路径映射关系清晰直观。不过生产环境部署时代理就不归前端管了。线上是把前后端打出来的静态文件部署到Nginx由Nginx把/api开头的请求反向代理到后端的服务端口。开发环境用Vite代理、生产环境用Nginx这两者之间的差别很多新手分不清但只要你完整部署过一次就永远不会忘记。联调时还有一个常见的摩擦点——时间格式。Java后端返回的时间默认是一串数字时间戳或者带毫秒的格式前端展示得格式化。为了避免各自处理造成不一致我在后端统一配置了spring.jackson.date-format把LocalDateTime序列化成了yyyy-MM-dd HH:mm:ss的格式前端拿到的就是人类可读的字符串直接展示。这类约定如果能提前定好联调能少吵好几回架。5. 部署运行与常见问题排查5.1 从源码到跑起来一步一步来你拿到源码想跑起来并不复杂按下面这个顺序走每一步都能验证成功后再进行下一步。第一步准备好运行环境。JDK 8、Maven 3.6、Node.js 16以上、MySQL 5.7或8.0这四个缺一不可。MySQL的安装这里多说一句Windows上安装5.7.44或者8.0时注意选择UTF-8字符集初始化数据库时把用户名密码记好最省心的方案是下载解压版配置my.ini不推荐用安装包默认配置因为自定义目录和数据文件位置会牵扯很多后续权限问题。第二步初始化数据库。打开Navicat或者命令行新建一个名为food_website的数据库字符集选utf8mb4然后把源码里提供的SQL文件导入。SQL文件里包含建表和基础示例数据导入后验证一下看看有没有报错如果提示SQL语法问题大概率是MySQL版本太旧建议升级到5.7以上。第三步配置后端。用IDEA打开后端项目等Maven把依赖下载完修改src/main/resources/application.yml里的数据库账号密码保证和本机MySQL一致。然后启动主类看到“Started Application in X seconds”的日志就说明启动成功了。此时浏览器访问http://localhost:8080/api/food/list?pageNum1pageSize10应该能返回JSON格式的菜品列表数据。第四步启动前端。命令行进入前端目录npm install安装依赖然后npm run dev启动开发服务器。浏览器访问http://localhost:5173能看到首页渲染出菜品数据和图片说明前后端已经联通。第五步验证登录和后台。先在前台注册一个普通用户然后登录尝试收藏一道菜和发一条评论。再用管理员账号登录后台看能不能在后台修改菜品数据。这一套流程走通项目就算完全跑起来了。5.2 我实际踩过的坑整理成速查手册开发这类前后端分离系统时我遇到的坑大概可以整理成下表。这些内容在官方文档里很难找全但都是真实开发中会反复遇到的问题症状可能原因解决方案后端启动报“端口被占用”8080端口被其他程序占用改application.yml里的server.port或找到占用进程关闭数据库连接报错“Access denied”账号密码错误或权限不足检查yml里用户名密码确认数据库账号允许本机连接中文乱码数据库字符集和连接URL不一致数据库建库用utf8mb4连接URL上加characterEncodingutf8前端页面打不开接口报CORS错误代理配置没生效检查vite.config.js中server.proxy配置确认前端重启过登录后刷新页面状态丢失只用了Pinia没做持久化登录信息存localStorage刷新时在初始化逻辑里重新读取上传的图片无法显示后端没配置静态资源映射后端配置addResourceHandlers映射上传目录为/upload/**时间显示和本地差8小时MySQL时区问题连接URL加serverTimezoneAsia/Shanghai后端返回JSON里多出很多空字段实体类属性未统一处理可用JsonInclude(Include.NON_NULL)过滤空值打包部署后前端路由404Nginx没配try_files配置location / { try_files $uri $uri/ /index.html; }后台修改菜品后前台看不到变化MyBatis二级缓存未刷新在修改操作里显式清除对应Mapper的缓存有一个值得单独说的问题就是数据库密码包含特殊字符比如、#时连接URL会解析错误。这种情况要么改一个纯数字字母的密码要么在yml里对特殊字符转义我第一次遇到时排查了很久最后发现是密码里的#被当作注释符处理了。这个细节没经历过的人不太会第一个想到。5.3 如果你想让这个项目再往前走一步这套系统已经把基础功能跑通了但它绝对不是一个封顶的项目反倒是扩展空间大得很。最顺理成章的扩展方向是给搜索加全文检索现在菜品列表的搜索是SQL里的LIKE模糊查询数据量几千条倒是没什么问题但如果菜品过万搜索体验就会明显下降。可以给菜名和描述字段建立全文索引或者上Elasticsearch但除非你真的有几千条以上的数据否则不建议提前上。第二个值得扩展的点是图片上传。目前图片上传后是存在本地磁盘的单机部署没有问题但如果以后部署到云服务器磁盘空间、备份和分布式场景下都会受限一个合理的升级方向是把图片存储迁移到OSS或者云存储服务数据库里仍然只存url路径改动量主要在文件上传的工具类上对整体架构影响很小。第三个方向是加入Redis。当前收藏数量和浏览量的统计是直接在MySQL里做UPDATE操作的如果并发写量上来行锁竞争是个隐患。用Redis做一个计数缓存先写Redis再定时同步回MySQL这是很经典的方案。不过对于当前项目规模先维持现有实现就好等你真正理解了缓存和数据库的一致性怎么做再来动这块也不晚。第四如果要做成多商户或者内容社区的形态——类似美食分享社区用户可以发自己的独家菜谱这就涉及到创作者体系、内容审核、关注关系这些更复杂的业务模型。底子里还是这几张表往上加但整个项目的价值感会上升一个档次。可以说当前这个美食网站是一个收得住的骨架也是一个随时能往外扩的起点。