做个人项目这些年前后端分离的练手项目做了不少但每次有人让我推荐一个既能完整跑起来、又能覆盖主流开发流程的学习项目我第一反应往往是这套美食网站系统。为什么因为它的技术选型非常贴近当下中小型项目的真实组合SpringBoot Vue MyBatis MySQL前后端完全分离纯BS架构。既不是烂大街的图书管理系统也不是一上来就上微服务的过度设计正好卡在一个“够典型、够实用、够好学”的位置上。这套系统我从拿到源码到本地跑通、再改造成自己的版本前后折腾了差不多一周。整体看下来它把用户注册登录、菜品分类展示、关键词搜索、菜品详情、下单流程、后台管理这些都串起来了对应的是一个完整电商类网站的纵向切片。也就是说你把它跑起来之后不只是看到一个页面而是能看到从前端页面交互、后端接口设计、数据库表结构、鉴权逻辑到最终打包部署的完整链路。这篇文章我会从架构设计、核心功能拆解、关键代码实现、部署流程和常见坑这几个维度把这个项目拆开揉碎了讲清楚。不管你是刚学完SpringBoot和Vue基础知识、想做第一个全栈项目的学生还是想快速了解前后端分离项目结构的初级程序员这套东西都值得你花时间过一遍。1. 项目整体设计与架构拆解1.1 为什么选前后端分离方案这套系统采用的是标准的前后端分离结构前端是Vue单页应用后端是SpringBoot提供的纯RESTful API两者通过JSON格式的数据进行交互。这种架构在现在的企业项目里几乎是默认选项了和传统的服务端渲染项目相比区别非常明显。先说说传统的写法。早期做JavaWeb项目JSP页面直接嵌在服务端Tomcat既管数据又渲染页面整个项目是一个高度耦合的整体。前端要改个样式后端得跟着重新打包发布后端改了接口逻辑前端页面也受影响。开发和联调都窝在一个项目里分工协作很难做。如果你面试或学习时能在聊天中把这个痛点讲清楚对方就能确认你是真干过活、真理解设计取舍的而不是只背概念。因为它直接关系到你拿到这个项目后能不能进行改造或者说你能否把你自己的UI想法独立地做进去。前后端分离的核心优势有三个。第一是独立开发并行推进后端定义好接口文档就能先行开发前端用Mock数据同步做页面。第二是独立部署和扩展后端接口可以做成集群前端页面放到CDN或任意静态服务器上。第三是职责边界清晰后端代码只关心业务逻辑、数据处理和权限校验前端的路由、状态管理、组件渲染全部由前端框架自己负责。这套系统选择前后端分离不是赶时髦而是因为美食展示类网站页面交互频繁图文混排、分类切换、搜索联想这些场景都非常适合用组件化的Vue来承载而后端的菜品管理、用户管理、订单处理又非常适合SpringBoot这类的分层架构来处理。1.2 技术栈选型的四个理由这套系统的技术栈选的是SpringBoot Vue MyBatis MySQL每一环都是有讲究的。SpringBoot负责后端接口层。现在Java后端做REST APISpringBoot基本是不二之选。内嵌Tomcat不用额外装容器自动配置机制省掉大量XML配置配合Maven直接一个命令就能启动。对于这个项目体量来说SpringBoot的启动速度和开发效率都是最优解。Vue作为前端框架核心优势在组件化和响应式数据绑定。美食网站的首页有导航栏、轮播图、菜品卡片、分类侧边栏等十几个独立区块用Vue的单文件组件来组织代码可维护性比直接写HTML加jQuery高出一个量级。Vuex状态管理可以用来存用户的登录状态、购物车数据Vue Router负责管理页面跳转。而且Vue的学习曲线相对平缓对刚从前端三件套过渡过来的开发者也友好。MyBatis这套持久层框架是让我最满意的一环。选择题主没有用JPA而是选了MyBatis这是非常有经验的、懂中国开发者现状和真实企业项目现状的选择。因为它执行过程透明、上手门槛低SQL都是由开发者自己掌控的。后端优化项目性能时尤其是列表分页、组合条件查询、多表连接查询把SQL拿出来稍微调整一下加个索引返回值差别就出来了这对于教学项目来说非常合适。它没有像JPA那样把SQL层层封装反而适合初学者看清楚每个操作到底对应了一次什么样的数据库交互。这个选择说明题主不是照搬技术栈是认真考虑过项目定位和用户档次的。MySQL作为数据存储这块没什么好说的。轻量、开源、生态成熟是这套系统当之无愧的绝配。本项目里数据的量级不大但涉及的表结构却比较丰富用户表、菜品表、分类表、购物车表和订单表之间都存在外键关联关系用MySQL来完整性约束、事务处理和索引优化都非常顺手。1.3 数据库表结构设计思路做项目第一步不是写代码而是数据建模。这决定了整个系统的骨架建得好不好直接影响后续开发的效率和代码复杂度。这套系统整体的表结构典型电商网站的味道很浓用户、菜品、分类、购物车、订单这几个核心表围成了一个完整的闭环。用户表属于最小必要实现一眼望过去毫不冗余。包含id、用户名、手机号、密码MD5加密、头像等字段。密码加密这一点是有安全意识的体现明文存密码在这个时代是完全不能接受的哪怕只是个学习项目。用户状态status字段也很实用给后台管理员实现禁用用户留下了扩展点。菜品表是核心业务表字段设计体现了对美食内容的足够理解。主图图片、描述、价格、月销量、分类id这些字段一个不少。月销量这个字段其实是整个推荐排序算法的量级级基础是理解整个美食展示逻辑的核心。分类表则是一个典型的树形结构或平铺结构项目包含了id、名称、描述、图标。它没有过度设计而是简洁地支撑了首页的侧边分类导航和菜品列表的分类筛选。购物车和订单表则构成了交易链路。购物车表通过user_id和dish_id与用户和菜品关联提交时生成订单。订单表的状态字段预留了支付、待发货、已完成等状态位的扩展空间。这种表关联模式是典型的电商业务建模思路放在实际项目中逻辑依旧是成立的。我为这个项目改造成甜品店版本时就是在菜品表加上甜度、温度两个自定义字段前端页面修正过后整个系统就完全换了副面貌。这也是我推荐学习项目一定要选贴近业务的类型的原因——它的变化会让你真正理解扩展性的分量。2. 核心业务功能与实现细节2.1 用户模块怎么设计的用户模块是这套系统功能闭环的起点也是理解整套项目权限体系最佳切入点。注册逻辑比较标准用户在注册页面填完用户名、手机号和密码后前端把数据POST到后端的/api/user/register接口。后端拿到数据后先在用户表中查一下手机号是否注册过如果已存在直接返回提示信息如果不存在就对密码做加密然后写入数据库。原来源码用的加密方式是MD5加盐的形式就是生成一段固定盐值拼到密码后面再做哈希。如果你要把这个系统接入互联网环境这个强度肯定不够建议升级成BCrypt。但这块作为学习项目演示登录注册流程是足够的。登录接口是/api/user/login验证通过后后端会签发一个JWT令牌返回到前端前端将令牌存在localStorage里。之后每次请求接口Web前端都会携带这个token后端拦截器对所有/api/**路径做统一校验。这个通过请求头传递校验凭证的机制从理论上确保了用户访问后端API的资格。实际操作中我在改造时在拦截器里顺带做了个简单的日志切面来记录每个用户每次的菜品查询行为。在没有改动核心代码的前提下日志就能记录到非常规整的行为序列。这其实是一个很实用的扩展例子。JWT拦截器这套组合就是典型的守卫整个系统用户身份识别的核心通路。不过在学习这个模块时我建议你多留一个心眼JWT本身是无状态的签发之后无法主动作废所以当你做用户封禁功能的改造时得先在用户表字段里加上status状态位然后在拦截器里加一层判断逻辑来读取并校验。这就是从学习项目到工业级项目演进时常见的一个代差值得你把它记录下来。2.2 菜品分类与列表展示逻辑美食网站的门面就是菜品分类和列表展示。用户打开首页最先看到的是分类导航和菜品瀑布流。这个功能的实现链路是前端页面挂载时调用后端/api/category/list接口拉取全部分类数据渲染到侧边栏点击某个分类页面再携带分类id去请求/api/dish/list?categoryIdxxx后端根据分类id做条件查询返回对应菜品列表。我最初以为菜品列表就是一条简单的SELECT * FROM dish WHERE category_id ?但实际上手后发现项目里还加入了分页逻辑用到了PageHelper这个分页插件。虽然只是简单了两行代码的事儿但确实会让你开始思考一个朴素的问题当美食网站的数据量上涨到万级时你的查询和展示性能还能不能被现在的代码支撑得住。分类的切换在业务上其实还不只是一个简单的点击切换。为了避免每次切换分类都重新请求菜品接口我用改造的方式测试过做好前端数据缓存、使用vuex维护每个分类下的菜品列表状态确实能显著减少没有必要的API请求。但缓存处理也会带来状态一致性问题比如后台改了某个菜品价格前端缓存里的数据还是旧的。这种取舍上的分析就是在学习项目里需要用脑筋去体会的东西它对你面试和实际工作中评估项目方案的收益与代价都非常有帮助。菜品卡片上的销量字段也是一个可以挖掘的信息点。系统里同步提供了按月销量排序的接口让热销菜品优先排在列表前面。这个非常基础但它就是一个最简浓度版的推荐系统了——它不谈算法复杂度也不提离线计算却提供了让后端基于一个字段在线排序的需求场景。尤其适合给想搞懂推荐原理的初级开发者提供直观感知。2.3 搜索与菜品详情功能搜索模块一定要拿出来单独说。这个系统里搜索功能我实测下来走的是模糊匹配的路线对应的接口是/api/dish/search?keywordxxx。后端用MyBatis的动态SQL做了关键词匹配LIKE CONCAT(%, keyword, %)对菜品的名称、描述等多字段进行查询。实现简单明了能跑能查但坦白讲性能和数据量一上去就会掉链子因为%xxx%这种SQL写法在数据量大时没有办法命中索引。我当时改造时把搜索升级成了倒排索引方案对接了一个轻量级的搜索引擎服务效果确实提上来了但同时也让代码复杂度上了一个大台阶。这里得给个平衡点的建议学习阶段先用老方案跑通始终把业务逻辑放在第一位后续有条件再做替代方案别一开始就追求工程复杂度。菜品详情页的价值主要体现在它的数据组织和页面信息的结构化上。接口/api/dish/detail?idxxx一次性返回菜品的完整数据包含标题、分类、图片、价格、描述、月销量等前端详情页再通过路由参数获取菜品id并请求该接口渲染。另外从菜品数据到用户自主下单的核心转换我理解下来就是购物车模块的职责。3. 关键代码实现与配置解析3.1 SpringBoot项目骨架与核心依赖后端工程标准Maven结构。要复现这个项目你只需要准备JDK 1.8及以上、Maven 3.6、MySQL 5.7和IDEA这些常规环境。核心依赖在pom.xml里是这样配置的parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency /dependencies选SpringBoot 2.7.18这个版本是一个很讲究的选择。它不是最新版但绝对是最稳定的版本之一。Java生态有个魔咒版本越新坑越多尤其是SpringBoot每次大版本升级都会有大量自动配置类的位置和名称变动。在你没有完全理解框架底层原理之前我建议你拿到这个项目就不要轻易动主框架版本号先跑通之后再考虑升级。MyBatis的配置方面主配置在application.yml中指定了mapper XML文件的位置和数据库连接信息。项目里使用XML文件的方式编写SQL而不是使用注解SQL理由和上面选型时提到的是一致的复杂SQL、动态条件查询的实现和调试离不开XML直观的SQL掌控力。3.2 JWT登录与全局拦截器JWT相关代码是整套系统里最有学习和转换价值的一块。整体流转过程是用户登录成功后服务端生成一个包含了用户ID和用户名的token再在响应中返还给前端前端保存在localStorage里。后续访问受保护接口时请求头里带上token后端经过拦截器解析逻辑确认身份有效性后才能放行。核心生成代码可以这样实现public class JwtUtils { private static final String SECRET your-secret-key; public static String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8))) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8))) .build() .parseClaimsJws(token) .getBody(); } }拦截器的核心逻辑就是对请求路径做判断。如果是/api/user/login或/api/user/register这些接口直接放行其余接口都要经过parseToken方法解析。一旦解析失败就会抛出异常由全局异常处理器统一返回401状态码。实际操作时拦截器主要应对的是“用户未登录访问后台管理页面”这类问题。满屏幕的安全隐患测试让我意识到一个问题不要把SECRET密钥硬编码在代码里。用环境变量或配置文件独立管理然后再在.gitignore里把它忽略掉这对防止密钥泄漏有直接作用。我在初学阶段就直接把密钥写进代码提交到仓库还好这只是练手项目。在这里把这个坑说出来希望你少走这个弯路。3.3 Vue页面与axios封装前端项目跑起来的脚手架是基于Vue CLI构建的。src目录下分为api、assets、components、router、store、views这些分类目录这个设计模式我比较喜欢是主流Vue项目的标准布局。前端和后端通信全部依赖axios封装。项目里api目录下有一个统一的请求配置文件初始化了axios实例设置了基础URL和请求头。最关键的是它通过axios拦截器每次请求都会从localStorage里取token动态添加到请求头里的Authorization字段。有了这层封装前端每个业务模块调用API的逻辑就纯粹多了只需要关心数据。// request.js import axios from axios import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }, error { return Promise.reject(error) }) service.interceptors.response.use(response { return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) }) export default service这段代码沉淀出来的模式直到今天我开发新项目时也经常沿用。注意看401的拦截处理后端返回401前端口径直接清掉本地token并跳转登录页。这个联动机制是你开发任何前后端分离项目时都值得固定下来的关键逻辑骨架。用户体验上失效登录后自动被弹回登录页从产品逻辑上是顺滑的这就可以直接沉淀成自己的代码资产。视图层模板首页由Home.vue承担包含分类侧边栏、菜品列表和轮播图。主要逻辑在两部分mounted阶段调用getCategoryList初始化分类数据点击分类切换时再调用getDishList获取菜品列表。购物车页面则负责读取vuex中存储的数据渲染出商品清单并支持数量调整和删除。整体代码结构清晰适合作为第二个手写项目来学习。4. 部署流程与常见问题排查4.1 本地开发环境准备部署这套系统前先把环境列个清单JDK 1.8或更高版本Maven 3.6以上Node.js 12以上MySQL 5.7以上以及IDEA或VSCode等开发工具。数据库初始化是第一步。项目里一般都会提供sql脚本文件里面包含了建库建表和基础数据的插入语句。执行时可以直接用Navicat或命令行工具。我在命令行环境实测过使用下面的命令效率最高mysql -u root -p dish.sql建议你执行完成后立刻去检查两张核心表的行数确认基础数据是否完整。如果初始数据没插进去后面前端页面打开就是一片空白排查起来浪费半天时间。后端启动前一定要去application.yml里核对数据库连接信息。用户名密码、数据库名称这些配置信息要改成你自己本机MySQL对应的实际信息。在这踩过坑的人不在少数项目代码里默认配置和在你自己机器上创建的数据库名称不一致后端启动直接报错连接失败。实际启动后端的命令mvn spring-boot:run看到控制台打印出Tomcat started on port 8080的日志就说明后端服务已经就绪。前端启动则相对简单。打开终端切到前端工程目录依次执行npm install和npm run serve。开发服务器默认跑在8080端口和后端默认的8080端口冲突的话可以在vue.config.js里修改前端端口也可以改后端端口。按我的习惯改前端端口为8081更省事补上一条代理配置请求后端接口就能跑通整个链路。4.2 后端打包与前端构建本地开发跑通之后部署需要打包。后端打包执行mvn clean package -DskipTests打包完成之后在target目录下会生成一个可执行的jar文件。这一步需要注意它本来就内置了Tomcat所以部署线上环境也丝毫不虚。前端构建执行npm run build构建产物会生成在dist目录主要包括静态JS、CSS、HTML等文件。在SpringBoot部署场景下可以有两种策略选择一是把dist目录里的文件直接放到nginx的html目录二是把dist目录里的文件复制到后端项目的src/main/resources/static目录中再把整个SpringBoot项目重新打包让它同时承担静态页面服务。第一种方案更符合前后端分离的架构精神接口请求走代理转发到后端服务。下面是一段标准配置server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; } }try_files指令是为VueRouter的history模式服务的。如果前端路由用的是hash模式这行可以去掉但考虑到URL美观度大部分人实际上会选用history模式这时这行配置就是非保留不可的定海神针。nginx监听80端口通过/api/前缀区分后端接口请求并将它们反向代理到你本地的8080端口。这个方案的好处是前后端完全独立后端挂了也不影响静态页面加载而且nginx本身的静态文件处理能力和稳定性都远超直接用SpringBoot托管静态资源。4.3 前端刷新404问题的解决部署阶段遇到的高频问题就是刷新页面时出现404。这个问题的根源在于VueRouter的history模式。前端路由跳转是在浏览器端完成的但如果用户直接刷新或手动输入URL浏览器会直接向服务器请求这个路径而后端服务器上根本没有对应的真实文件。解决办法就是在nginx配置中加上try_files $uri $uri/ /index.html;把一切请求都先映射到index.htmlVueRouter再根据URL去渲染对应的页面。如果你采用的是把前端构建产物放到SpringBoot的static目录里打包的方案那么需要同时在后端加一个控制器确保所有非/api开头的请求都转发到index.html。这在SpringBoot中实现起来特别简洁。注意这个顺序很重要必须先判断/api开头的请求优先交给它们对应的Controller再把页面请求转发到首页模板。4.4 常见问题排查与技巧实录把我在整个部署实操中遇到过的典型问题整理成一张速查表按重要性排开现象可能原因解决方案后端启动报数据库连接失败账号密码或数据库名不对检查application.yml配置与本地MySQL相关配置保持一致前端请求接口报404代理配置问题或后端未启动先确认后端接口能通过curl访问再检查devServer代理登录后刷新页面又跳回登录页token存储键名不一致确认登录时存入和请求拦截器读取的是同一个key菜品图片不显示图片路径为绝对路径改成相对路径或使用线上图床刷新页面404Vuerouter history模式部署缺少fallbacknginx加try_files或后端加转发中文乱码数据库字符集不是utf8mb4建库时指定字符集重启MySQL服务时间字段显示不正确时区设置问题jdbc连接串加serverTimezoneAsia/Shanghai前端端口被占8080端口被后端占用修改vue.config.js里端口或后端端口数据库乱码是一个值得多说两句的坑。如果你在建库时没有显式指定字符集就是mysql默认的latin1中文字段全部变成了问号。建库的时候记住执行这样一条固定写法CREATE DATABASE dish_website DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;同时数据库连接URL上也务必添加characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai。这两步一起配合绝大多数乱码问题的原因就都在这里不必再看其他地方。还有一个平时不容易发现的问题是图片路径。项目源码里的菜品图片多半是本地相对路径如果你直接把项目部署到服务器上图片路径就会失效。我当时的处理方式是把图片传到一个公共资源服务器然后统一替换前端代码里的图片链接前缀这样部署在任何环境都能正常访问图片了。5. 项目扩展与学习建议5.1 可以动手扩展的方向这套项目极其适合作为二次开发的基底项目我改造过两个扩展方向可作为参考分享。第一个方向是给后台管理添加文件上传功能。原来源码里的菜品是管理员直接在数据库里插入的没有可视化后台因为后台管理里没做菜品图片上传。改造方案很简单在后台的表单里加图片上传组件然后通过SpringBoot的MultipartFile接口把图片文件保存到服务器本地或对象存储里最后把访问URL存到数据库的相应字段中。动手做一次文件上传实现对你后续理解静态资源分离、OSS对象存储都会有实质性帮助。第二个方向是把用户收藏功能加上。给用户表和菜品表之间添加一张收藏关联表在菜品卡片上增加一个收藏按钮个人中心里再做个收藏列表页面。这样你就把“多对多”关系表彻底练熟了——用户和菜品之间不再是简单的一对多关联而会是你在实际开发里时刻都会遇到的核心业务场景。上了这个台阶以后理解订单明细、标签系统、权限角色这类多对多模型基本是一通百通。再往深一步说如果你是准备求职的同学我强烈建议你选一个模块把它推向“可上线”的水准。比如给商品列表增加多条件组合筛选把分类、价格区间、销量排序组合在一起再配合分页接口改造。面试时讲清楚这条链路的SQL实现、索引策略和缓存设计绝对比你背一万个八股文更有说服力。5.2 学习这套项目的正确姿势项目源码拿到手最忌讳的做法是直接npm run serve和mvn spring-boot:run跑起来看一眼就关掉。这样学不到任何东西。我分享一套我实际带人常用的方法不绕弯子地讲先看数据库脚本把每张表的字段含义理清楚标注出表与表之间的关联关系。单独建一个思维导图或表格笔记画出表关系图。这一遍不用读代码但你要能回答出“菜品表里为什么要有category_id”这类问题。然后写接口清单。对照后端Controller把每个接口的请求方式、路径、参数、返回类型全部列成表格。这个表格就是你理解整个后端功能的索引。你会惊讶地发现整个系统的接口加起来其实不超过二十个之前觉得高不可攀的全栈项目抽象成接口之后瞬间就清晰了很多。接着独立复刻核心流程。关掉源码参考前先去实现一个最简单的用户注册登录。遇到问题再翻源码里是怎么处理的。这个循序渐进的写法比逐行逐行读代码的效率和吸收率高太多。完成后再尝试加一个“今日推荐”模块这个完全由你自由发挥的模块才是真正打通你全栈思路的关键点。最后做一次生产环境的模拟部署体验完整的发布流程打包、上传服务器、配置nginx、启动服务、访问域名。到这里你会发现这个项目才真正完整体验完了。写在最后的一点心得体会这个项目我前前后后看了几天从看表结构到跟着接口调试再忙里偷闲改造成自己的甜点店版本整体下来的感受是它完全不是那种只能应付毕设的玩具项目它是把一个真实业务系统做了一次恰到好处的简化。业务链路是完整的技术选型是克制的代码结构是可扩展的部署方式是标准的。对初学者来说它把前后端分离这个概念从抽象的名词变成了看得见摸得着的工程实现。最后再分享一个我在部署实践中觉得特别舒心的细节这套项目跑起来后前端的首屏加载速度、接口响应耗时表现都很不错。原因在于静态资源少、依赖简单、图片路径清晰、接口查询不复杂。虽然它本身不算什么重量级的性能优化案例但能让初学者直观感受“一个没有被各种复杂依赖拖累的项目跑起来应该是什么感觉”。我一直觉得一个练手项目能让你对未来真实项目的样子有了体感它就是称职的。希望这篇拆解能帮你少走一些我走过的弯路。