ARTICLE DETAIL

资讯详情

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

Spring Boot+Vue美食网站开发:从数据库设计到前后端分离部署实战

Spring Boot+Vue美食网站开发:从数据库设计到前后端分离部署实战 1. 项目概述与核心功能拆解看到“Java基于Spring BootVue的美食网站的设计与实现”这个题目我第一反应是这应该是课程设计或毕业设计级别的项目但它又跟那些换壳的“某管理系统”不太一样。美食网站本质上是电商系统的迷你版用户能浏览菜品、加购物车、下单管理员能维护菜品和订单。这个规模对初学Spring Boot和Vue的人来说恰好覆盖了“业务接口—数据存储—前端页面—权限控制”完整链路做完了能讲的东西非常多面试时也是个好话题。1.1 一个美食网站到底在做什么我见过太多直接把海鲜、火锅、快餐换个主题就交上来的“同款项目”说实话那种项目做完自己都没什么成就感。真正有价值的做法是先把场景想清楚用户用浏览器逛美食网站按分类浏览菜品、看菜品图片和详情、搜索想吃的菜、把菜加入购物车并下单管理员登录后台维护菜品分类、菜品信息、上传菜品图片、管理订单状态。从功能模块上看这个项目至少要分成两端前台用户端注册登录、首页推荐菜品浏览、按分类/关键词搜索、菜品详情页、菜品评论、收藏菜品、加入购物车、生成订单、个人中心查看订单列表。后台管理端管理员登录、菜品管理增删改查、上下架、上传图片、分类管理、用户管理、订单管理状态流转、简单的销量统计。这个功能划分不复杂但把电商系统的主链路完整串起来了。答辩PPT上写“基于Spring Boot Vue的前后端分离美食电商系统”比我看到过的那些“美食展示网站”听起来专业得多。1.2 功能设计里的取舍与关键细节功能清单列出来之后有几个细节必须提前想清楚否则后期全是要命的返工。第一个细节是订单状态字段。常见状态有“待支付、已支付待处理、已完成、已取消”如果是外卖点餐场景还可以加“配送中”“已送达”。我建议用int类型存状态码在代码里定义一个常量类比如0-待支付1-待处理2-已完成3-已取消。不要直接在数据库里存中文字符串后续统计、扩展、改状态流转都会很难看。第二个细节是菜品分类。最实用的方案是单级分类“家常菜、主食、饮品、汤羹、果盘”就够用了不要一上来就整树形无限级分类。树形分类虽然技术上听起来有亮点但它会给前端菜单、后台列表、搜索过滤链路都带来不小的复杂度。单级分类已经能完美支撑“按分类浏览”的核心需求省下来的时间投给菜品图片上传、评论功能会划算得多。第三个细节是用户和管理员怎么区分。不建议单表加role字段的粗糙做法但也不太建议完全拆成两套认证体系。我通常用一张user表加role字段0代表普通用户1代表管理员。登录接口统一处理拦截器根据接口路径前缀决定是否需要管理员角色比如/api/admin/**必须role为1既简单又实用。功能梳理到这里基本可以进入技术选型了。很多新手在选型这里总是纠结半天我下面把真实做这个项目时的选型理由理一遍。2. 技术选型Spring Boot Vue这对组合强在哪为什么这个题目点名了Spring Boot和Vue因为它们是目前Web开发里最顺手、案例最多、面试最好讲的一套组合。对课程设计、毕业设计、甚至中小型真实项目来说这套组合都是非常稳妥的答案。2.1 后端Spring Boot凭什么好用Spring Boot解决了传统Spring项目最痛苦的配置问题。我早年做第一个SSM项目时光springmvc.xml、applicationContext.xml、mybatis-config.xml就配了一整天依赖版本稍微不对就是一个大红叉。Spring Boot通过自动配置和starter机制大幅降低上手成本你只需引入spring-boot-starter-web写一个带RestController的类就能立刻暴露一个HTTP接口这对学生和刚转行的人来说是极大的信心来源。用一句话总结Spring Boot自带Tomcat默认端口8080不需要外部容器依赖是“全家桶”式管理引入starter就能自动完成大部分配置。再加上社区极其成熟遇到问题基本一搜就有答案。对新手来说这种“查得到解决方案”的安全感比什么都重要。2.2 前端Vue为什么受欢迎Vue相比传统jQuery时代手动操作DOM的方式最大的优势是“数据驱动视图”。这句话听着玄实际体验就是你只需要维护好data里的一个foodList数组页面上用v-for渲染出来的菜品列表就会自动跟着变化不用手动拼接HTML再往页面里塞。Vue的学习曲线也比较友好。很多搞Java的人对后端思维很熟练但看到JS的原型链、闭包就头大。Vue的语法把这些包装得很“友善”再加上Vite/Cli脚手架几圈命令下来一个带路由、组件化、热更新的前端工程就立起来了。Vue在国内的开发群体非常大遇到问题包括各种中文报错、插件配置几乎都能找到现成答案。2.3 前后端分离架构的关键认知前后端分离并不神秘核心就是“前端只负责展示和交互后端只负责数据和业务逻辑”。两者通过JSON格式的HTTP接口通信。这里有个概念必须想明白跨域CORS。前端跑在http://localhost:5173Vite默认端口后端跑在http://localhost:8080浏览器会拦截跨域的AJAX请求原因就是浏览器默认的“同源策略”。解决方式有三种后端添加全局CORS配置允许指定前端端口访问。前端开发时配置代理把/api请求转发到http://localhost:8080。生产环境把前后端部署在同一个域名下通过Nginx做反向代理。我个人建议开发时用方式2部署时用方式3这样能避免很多“本地能跑部署就跨域”的玄学问题。选型讲完下一步就是数据库设计。这一步是重中之重表结构没理清楚后面写代码全是返工。3. 数据库设计表结构比代码更需要耐心先讲一个我帮人改过的真实案例对方把菜品表设计成“每种分类一张表”叫jiuchang_table、zhushi_table一共造了十几张表。结果代码里就要写十几个Mapper方法新增一个分类还得建表和写接口。这种设计基本等于给自己挖坑。正确做法是把“分类”抽象成category表的一个字段而不是把分类变成表。3.1 核心表有哪些一个标准的美食网站核心数据表大约5到8张我按“最小可用且答辩能过”的标准列一下user用户表category菜品分类表food菜品表orders订单表order_detail订单明细表comment评论表collect收藏表cart购物车表如果你做得比较克制甚至可以把收藏表和购物车表合并成一张user_food关联表用一个type字段区分是收藏还是加购。但我个人更建议分开原因很简单购物车有数量、更新时间等业务字段收藏只需要关联关系强行合一张表会出现大量的“空字段”表结构不够干净后端代码写着也别扭。3.2 核心表字段说明下面把最重要的两张表摊开讲其他表按同样的思路去推导就行。user表用户表字段类型说明idbigint主键自增usernamevarchar用户名唯一passwordvarchar密码BCrypt加密roletinyint0普通用户 1管理员avatarvarchar头像URLemailvarchar邮箱可空phonevarchar手机号可空create_timedatetime注册时间密码一定不要明文存储。我见过太多项目在数据库里直接存明文密码一旦数据库泄露用户在不同平台使用相同密码的账号就全完了。Spring Security自带的BCryptPasswordEncoder一行代码就能加密这一点应该成为一种习惯而不是加分项。food表菜品表字段类型说明idbigint主键namevarchar菜品名category_idbigint所属分类IDpricedecimal价格建议decimal(10,2)imagevarchar图片URLdescriptiontext菜品描述stockint库存/份数salesint销量用于排序推荐statustinyint1上架 0下架create_timedatetime创建时间这里有个特别重要的坑价格字段请用decimal不要用float或double。float和double本身是科学计算用的二进制浮点数用来存钱会出大问题。你算一下0.1加0.2等于多少浮点误差直接让“1块钱的包子”变成“0.999999999元”。这个知识点不仅做项目要用面试也偶尔会问。3.3 关系与外键处理food表的category_id关联category表order_detail表通过food_id关联food表这是基本关系。但很多有经验的后端开发在真实建表时并不会真的添加数据库外键约束。原因是外键检查在高并发插入、删除时会带来额外开销而且开发时外键约束会限制“改数据”的灵活性。常规做法是在应用层做逻辑校验数据库只用索引维护关联字段。这里还有一个“内行”才知道的技巧order_detail里的food_name、food_price在创建表时一定要冗余进去。为什么因为订单一旦生成管理员之后可能改菜价、下架甚至删除这道菜但订单里的历史记录必须保持不变。下单时把价格和名称快照到明细表里后台报表统计才准确用户查历史订单时才不会看到“价格已变动”的尴尬情况。数据库设计这一关过了后面写代码的速度会非常快。下面进入真正的后端实现。4. Spring Boot后端核心骨架与重点接口实现一个Spring Boot美食网站后端可以粗略拆成五层Controller接口层、Service业务层、Mapper数据访问层、entity实体类、common通用返回/异常/工具类。我建议直接使用MyBatis-Plus而不是原生MyBatis。不是原生MyBatis不好而是MyBatis-Plus把常见的CRUD通用方法都做好了你写userMapper.selectById(1L)就能查用户完全不用自己写XML。对美食网站这种“CRUD不少、但又不算海量数据”的项目来说能省下大量无意义的时间。省出来的时间你可以用来做图片上传、评论、登录拦截这些能在答辩时“讲故事”的功能。4.1 项目初始化建议创建项目时直接去Spring Initializrstart.spring.io勾选依赖或者用IDE里集成的Spring Initializr。依赖建议选Spring Web、MySQL Driver、Lombok。如果走MyBatis-Plus路线就手动加MyBatis-Plus的坐标不用选MyBatis Framework。认证方案我建议先用“JWT 拦截器”而不是一上来就引入整套Spring Security。Spring Security功能强大但过滤器链、配置方式对新手来说极其劝退。JWT的签发和校验其实几十行代码就能实现配合一个拦截器就能控制接口的登录权限和管理员权限不但好理解答辩时也容易讲清楚。4.2 统一返回封装与全局异常前端最怕后端返回乱七八糟的字符串或HTML错误页面。所以我们需要一个统一的返回结构所有接口的成功和失败都返回同一套JSON格式Data public class ResultT { private Integer code; // 200成功400参数错误401未登录500服务器错误 private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(Integer code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }再配一个RestControllerAdvice全局异常处理类把业务异常、SQL异常、参数校验异常统一捕获。这样前端永远拿到的都是同一个结构的JSON解析起来非常轻松。我最初做项目时没有做这一步导致前端拿到错误时经常卡在undefined光调试就浪费了整整一个下午。4.3 JWT认证与登录接口JWT的全称是JSON Web Token简单说就是把用户身份信息加密成一段字符串后端返回给前端前端每次请求把它放在HTTP头Authorization里。登录接口的完整流程是前端POST用户名和密码到/api/auth/login。后端查用户用BCryptPasswordEncoder.matches()校验密码。校验通过后生成JWT把token和用户基础信息一起返回。核心生成代码大致是这样// 密码校验BCrypt if (!passwordEncoder.matches(rawPassword, user.getPassword())) { return Result.error(401, 用户名或密码错误); } // 生成JWT String token Jwts.builder() .setSubject(user.getId().toString()) .claim(username, user.getUsername()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact();然后写一个拦截器注册到Spring MVCpublic class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; // 放行预检请求否则跨域必挂 } String token request.getHeader(Authorization); // 解析token失败则返回401 return true; } }在WebConfig里注册拦截器时注意要排除登录、注册、首页菜品列表这些公开接口。还有一个细节很关键拦截器里一定要放行OPTIONS请求。跨域请求在正式POST之前浏览器会先发一个OPTIONS预检请求如果你在拦截器里把OPTIONS拦截了前端永远只会看到跨域错误。4.4 菜品与订单的核心接口设计接口设计有一条“前端友好”的原则尽量用资源名词加HTTP动词而不是写一堆/getFoodList、/addFood这样的动作接口。下面给出这个项目推荐的一套接口方法路径说明POST/api/auth/login登录POST/api/auth/register注册GET/api/food/list菜品分页列表GET/api/food/detail/{id}菜品详情GET/api/food/search搜索参数keyword/categoryIdPOST/api/cart/add加入购物车GET/api/cart/list购物车列表POST/api/order/create创建订单GET/api/order/my我的订单POST/api/comment/add评论菜品POST/api/admin/food/save管理员新增/修改菜品DELETE/api/admin/food/{id}管理员删除菜品POST/api/admin/food/upload上传菜品图片用POST加JSON统一处理新增和修改比“新增一个接口、修改一个接口”的写法干净很多。实体类里id为空就insertid非空就update一个接口搞定两种操作这也是我在做偏CRUD项目时的习惯写法。4.5 图片上传的坑美食网站最大的特点是图片多图片上传是不可回避的核心功能。Spring Boot接收上传文件本身很简单PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { // 1. 校验文件类型只允许jpg/png等 // 2. 生成随机文件名防止重名覆盖如 UUID 后缀 // 3. 保存到本地目录如 /usr/local/food/images/ // 4. 返回可访问的相对URL如 /images/xxx.jpg }但这里最容易被初学者忽略的坑是Spring Boot默认静态资源目录是classpath:/static/你把文件写到D:/upload/xxx.jpg前端访问http://localhost:8080/images/xxx.jpg一定会404。解决办法有两种把上传目录加到Spring Boot静态资源映射里具体是在WebConfig重写addResourceHandlers把/images/**映射到本机的磁盘目录。部署时通过Nginx把/images/路径指向服务器磁盘目录。实话说Nginx方案生产环境更靠谱但本地开发用第一种方案调试最快。两条路都建议亲手配一遍因为图片在上线环节永远会出各种幺蛾子。后端这套做下来基本所有接口都能通了。接下来才是用户真正感知的部分Vue前端页面。5. Vue前端从0到1搭一个美食门户5.1 项目创建与依赖安装Vue项目脚手架现在主流是Vite创建命令可以用npm create vuelatest # 根据提示选择Vue3 Router Pinia npm install npm install axios npm install element-plus很多Java开发者对Node工具链不熟其实只需要记住三件事npm install是装依赖npm run dev是启动开发服务器npm run build是打包。这三个命令能流畅跑起来前端开发就成功了一半。个人建议直接用Vue3的script setup语法这种写法比Vue2的Options API简洁很多尤其适合写页面结构偏重复的管理后台代码量能少三分之一。5.2 axios封装与请求拦截前端所有请求建议统一走一个封装好的axios实例目的有两个一是统一带token二是统一处理错误。// request.js import axios from axios const request axios.create({ baseURL: /api, // 开发时走Vite代理 timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { // 跳转到登录页 } return Promise.reject(error) } )这里有一个细节前端代码里统一写/api开头不要硬编码localhost:8080这种具体地址。开发环境用Vite代理转发生产环境用Nginx代理转发同一个前端代码在不同环境都能跑否则换个环境就得全局改URL改到怀疑人生。Vite代理在vite.config.js里配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }5.3 路由设计与权限守卫使用vue-router路由按页面角色分成两组用户端首页/、菜品列表/food、菜品详情/food/:id、购物车/cart、我的订单/orders、登录/login、注册/register。管理端/admin下嵌套子路由包含菜品管理、分类管理、订单管理、用户管理。管理端路由用meta.requiresAdmin标记然后在router.beforeEach全局前置守卫里做权限校验router.beforeEach((to, from, next) { const token localStorage.getItem(token) const userInfo JSON.parse(localStorage.getItem(userInfo) || {}) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.requiresAdmin userInfo.role ! 1) { next(/) } else { next() } })5.4 菜品列表页的核心实现菜品列表页的经典组成是搜索框、分类标签、菜品卡片网格。用Element Plus组件能很快搭建script setup import { ref, onMounted } from vue import { getFoodList } from /api/food const foods ref([]) const categoryId ref() const keyword ref() const loadFoods async () { const res await getFoodList({ categoryId: categoryId.value, keyword: keyword.value }) foods.value res.data } onMounted(loadFoods) /script template div classfood-grid el-card v-foritem in foods :keyitem.id classfood-card img :srcitem.image alt菜品图片 / h3{{ item.name }}/h3 p classprice{{ item.price }}/p p classsales已售 {{ item.sales }}/p el-button clickgoDetail(item.id)查看详情/el-button /el-card /div /template这就是Vue响应式的魅力foods数组一变页面卡片自动增删用户感受就是“无需手动刷新数据秒级更新”。相比传统jQuery时代手动操作DOM这种体验完全是两个时代。5.5 管理后台怎么做管理后台最省力的方案是直接套vue-element-admin模板它有登录、权限、动态路由、表格组件、图片上传全套功能。但说实话如果你是这个项目的主人我更建议后台自己写。页面无非就是“表格加弹窗表单加图片上传加删除确认”用Element Plus的el-table、el-dialog、el-form组合非常顺手。后台最核心的“菜品管理”页面逻辑如下页面加载时请求getAdminFoodList把数据渲染成表格。点击新增或编辑弹窗显示表单编辑时回填当前行数据。提交时调用/api/admin/food/save接口。删除前用ElMessageBox.confirm二次确认再调删除接口。这一步本质就是反复做CRUD难度不大但把角色权限、表单校验、图片上传都串起来之后项目完成度会明显上一个台阶。前端做完前后端就该联调了。联调阶段通常才是“崩溃高发期”我顺手把常见问题集中整理一下。6. 前后端联调与部署上线要点6.1 开发时如何调试联调阶段提升效率的第一大利器就是把浏览器开发者工具的Network面板用好。前端看到页面空白第一步不是改代码而是看Network里请求是红色还是200。红色就点进去看状态码404说明路径写错了500说明后端报错了403或401说明权限没带上。后端调试方面Spring Boot的SQL日志特别关键。建议在application.yml里把SQL打印打开mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样每条SQL语句都会打印到IDEA控制台排查“查不到数据”“条件不对”非常直观。我见过太多人对着页面反复刷新却不知道后端SQL实际执行了什么浪费大量时间。6.2 生产环境部署部署其实并不复杂记住五个步骤前端执行npm run build生成dist/目录。后端执行mvn clean package -DskipTests打成可执行jar包。把dist和jar包上传到Linux服务器。后端用java -jar food-server.jar启动或者用systemd托管。Nginx配置location /指向dist静态文件location /api/反向代理到http://127.0.0.1:8080。Nginx配置示例server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; # Vue路由history模式必需 } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /images/ { alias /usr/local/food/images/; # 图片目录 } }try_files $uri $uri/ /index.html;这行配置是必须写的。Vue Router用的是history模式用户直接刷新/food/detail/3时Nginx在磁盘上找不到这个文件路径会返回404这行配置能让所有路径都回退到前端入口页面。不知道这个配置的人部署完总会遇到“刷新就404”的怪问题。6.3 数据库导入与交付规范数据库建议用SQL文件交付。在Navicat或DataGrip里把表结构导出成food.sql连同初始化数据脚本一起提交。我交付项目时还会写一个README.md里面写明数据库版本、JDK版本、Node版本、启动步骤。这几个信息缺一个对方都可能卡在环境上。很多人觉得README是多余的实际工作中一份清晰的README能让你少回无数条“怎么跑不起来”的消息。7. 常见问题排查与经验速查表这一节是我个人建议反复看的。代码是自己写出来没错但坑真的是踩出来的。7.1 跨域报错CORS头缺失现象页面能打开但接口请求报错浏览器提示Access to XMLHttpRequest... has been blocked by CORS policy。原因和解决办法依次排查登录拦截器是否拦截了OPTIONS请求如果是必须放行。后端是否配置了全局CORS可以在WebMvcConfigurer里重写addCorsMappings方法。前端是否直连了localhost:8080而不是走代理把请求改成/api前缀并配置Vite或Nginx代理。7.2 图片上传成功前端却看不到现象上传接口返回了URL浏览器打开却是404。原因基本是静态资源映射没配好或者路径拼接错误。对比一下数据库存储的图片路径应该是/images/20250101/uuid.jpg这种相对路径前端页面用img src/images/xxx.jpg匹配后端静态资源映射。如果是本地保存到D:/upload就按前面讲的方式配置addResourceHandlers。7.3 表名和SQL关键字冲突这个坑发生率不低。表名如果叫order正好是SQL关键字跑select * from order一定报错。解决办法很简单表名改成orders或者在实体类上用TableName(orders)显式指定。我个人的习惯是命名时直接绕开所有SQL关键字不给这种低级但气人的bug留机会。7.4 前端购物车刷新后数据丢失现象页面刷新后购物车空了。原因购物车只存在了Pinia或Vuex这类内存状态里一刷新就没了。解决办法是把购物车数据存到后端数据库登录用户每次请求购物车列表都从接口拉取。只有未登录的临时浏览数据才用localStorage兜底这是通用且成熟的方案。7.5 分页查询的总数不准现象分页列表的数据对不上count总数和实际列表不一致。排查方向有两个一是条件参数是否在count和page查询中同时生效二是MyBatis-Plus的分页插件是否正确配置了PaginationInnerInterceptor。如果分页插件没注册你会发现分页根本不生效而且不报错特别让人抓狂。8. 做完这个项目真正带走的是什么别急着关掉文章最后这一点是我个人的真实体会。这个项目的价值不在于“又做了一个管理系统”或者“答辩过了就行”而在于它完整覆盖了一名后端开发者从零到一的日常设计表结构、写REST接口、处理权限、联调接口、部署上线。我这些年带学生做项目时最常说的一句话是“你先别急着写代码把表结构和接口清单列出来。”因为这个项目确实不复杂但如果跳过设计直接开写你会体验到“用户要求加个菜品标签代码却牵连七八个文件”的痛苦。反过来只要表结构和接口设计想清楚了后面的编码就像填表一样顺。另一个建议是做完之后写一份简短的README和接口文档。这个习惯看似额外花时间但在答辩、找工作面试、甚至以后自己复用代码时价值都会翻倍。你面试时能流畅说出“我设计了8张表22个接口用JWT做登录态图片上传用静态资源映射解决”这种具体的话比一句“我做过项目”有说服力得多。最后分享一个小技巧如果做完核心功能后发现时间还有富余试着给项目加一个“销量排行”或“评论点赞”功能。这类小而亮的功能代码量不大却能成为项目汇报里让老师或面试官记住你的关键细节。一个能被人记住的亮点往往就是项目从及格走向优秀的距离。
返回列表