☰
SpringBoot+Vue+MyBatis+MySQL美食点餐系统落地实战
2026/10/15 4:56:54 网站建设 项目流程

这阵子有朋友喊我帮他做一个小餐饮店的线上点餐系统,聊着聊着就变成了一个完整的前后端分离项目。最后我按“SpringBoot+Vue+MyBatis+MySQL”这套经典技术栈,实现了一款B/S架构的美食网站系统,前台能看菜、加购、下单,后台能管菜品、管分类、管订单。源码和部署方式都整理好了,这篇文章就把整个开发过程和部署路径掰开揉碎讲一遍。

如果你是正在做课程设计、个人作品集,或者单纯想用一套真实业务把前后端分离吃透,这篇文章应该能让你少走不少弯路。全文不会只贴代码,更重要的是讲清楚每个设计决策背后的理由,以及那些文档里不会写、只有实际部署才会撞上的坑。

1. 从一家小餐馆的点餐需求说起:这套系统的功能定位与业务边界

1.1 核心业务流程拆解

我拿朋友的小餐馆当原型:店里没有专门的收银系统,客人到店看纸质菜单,点完菜靠喊,高峰期经常漏单、错单。他想要的很简单,让客人用手机或平板自己看菜、自己下单,后厨能看到订单就行。

这个需求落到系统上,就变成这样一条主流程:用户进入网站 → 浏览菜品分类 → 查看菜品详情 → 加入购物车 → 提交订单 → 商家在后台看到订单并更新状态。听起来不复杂,但要把这条链路做成“能真实使用”而不是“演示Demo”,涉及的数据表、接口、页面状态有好几十个。

我最终把系统边界划定为两大块。一块是面向普通用户的C端:注册登录、菜品浏览、菜品搜索、购物车管理、订单提交与查看。另一块是面向管理员的B端:菜品分类管理、菜品上下架与库存维护、订单状态处理、首页轮播图配置。没有做在线支付对接,因为小餐馆实际场景是“线上下单、到店付款”,支付留给后续扩展。

1.2 两种角色与对应的功能地图

角色设计得很朴素:普通用户和管理员。用户侧功能:

  • 注册与登录,登录后购物车和订单才跟人走
  • 首页轮播图推荐、热门菜品展示、分类导航
  • 菜品列表支持按分类过滤,也支持关键词模糊搜索
  • 菜品详情页展示图片、价格、描述、剩余库存
  • 购物车支持增删改数量,提交订单时会联动扣减库存
  • 个人订单页能看到订单状态(待处理、已完成、已取消)

管理端不另开单独端口,而是做成同一个前端工程下的不同路由,根据登录用户角色来控制页面可见性。管理员进入后台后,可以:

  • 对菜品做增删改查,修改价格、库存、上下架状态
  • 管理菜品分类,调整排序
  • 查看所有订单,标记订单为“已完成”或“已取消”
  • 配置首页轮播图

这样设计有个好处:前后端只有一套部署入口,开发和联调成本低。如果你想把它拆成独立的运营后台,路由和接口层面预留了扩展空间。

1.3 为什么B/S架构在这种场景下最合适

我见过很多同类项目纠结要不要做成客户端App,但B/S在这类业务里的优势太明显了。

首先是零安装。客人用手机浏览器打开网址就能点餐,不用下载App,不用关注公众号;管理员在电脑上打开后台就能管菜品,平板也能用。其次是升级成本低。前后端分离后,前端页面更新只需要重新构建静态文件,后端逻辑升级只需要替换服务进程,互不阻塞。

还有一个容易被忽略的点:B/S架构天然适配“多端展示”。同一套前端代码,在手机上显示为移动端布局,在PC浏览器上能完整展示后台管理界面。我实际测试过,用 iPhone 和 Android 手机打开前端首页,响应式布局表现得很稳,客人用起来不会有“这是电脑页面”的违和感。

这个系统的定位我建议你不要加太多东西。一开始我也想过加会员积分、团购、优惠券,后来全砍了。一个小型餐饮场景的核心是“菜品—购物车—订单”这条数据流,把这条链路做扎实,比堆一堆花哨但用不上的功能有价值得多。

2. 技术选型的账本:SpringBoot+Vue+MyBatis+MySQL这条链路值在哪

2.1 后端框架:SpringBoot解决的是“起步”和“集成”的痛点

我做后端很多年,早期用SSH、SSM搭项目,光配置XML就够折腾半天。SpringBoot的核心价值不是“新”,而是把常用组件的自动配置做掉了。内嵌Tomcat,不需要外置容器,写一个main方法就能启动服务;依赖管理用Starter,引入一个坐标就能获得一整套能力。

对这类前后端分离项目来说,SpringBoot让团队协作变简单了:后端同学只需提供Restful API接口,前端同学只需要跟接口文档对接。项目里我用了spring-boot-starter-web、spring-boot-starter-validation、mybatis-spring-boot-starter、mysql-connector-java这几个核心依赖,没有引入多余的东西,避免“依赖爆炸”。

版本选型我特意选了Spring Boot 2.7.x。如果你想照做,注意Spring Boot 3.x要求JDK 17,而2.7.x在JDK 8/11下都能跑。很多生产环境还是JDK 8,用2.7.x兼容性更好,资料也多。

2.2 数据访问层:MyBatis在订单类业务里比JPA更“心中有数”

MyBatis和JPA之争一直存在。这个项目我坚定选了MyBatis,理由是SQL可控性和动态SQL能力。

美食网站这类业务有大量组合查询:用户可能同时按“分类”“价格区间”“关键词”“排序方式”筛选菜品;后台也许要看“某个分类下所有已上架且库存不足的菜品”。用JPA写这种查询,要么拼方法名,要么上Specification,要么写JPQL,绕来绕去很别扭。MyBatis直接把SQL写在Mapper XML里,参数和结果映射清清楚楚,优化SQL也直观。

动态SQL更是我的主力工具。后面接口实现部分我会给具体示例,像多条件查询、批量插入订单明细,用<where>、<foreach>这类标签可以省掉大量字符串拼接。更重要的是,SQL是DBA能直接看懂的东西,出问题排查起来比ORM生成的SQL快得多。

2.3 前端框架:Vue给前后端分离带来的核心价值

前端我用的Vue 3 + Vite + Element Plus。Vue的响应式数据绑定让页面状态管理非常顺手:菜品列表加载后页面自动渲染,购物车数量变化后总价自动更新,这些在jQuery时代要手动操作DOM,在Vue里只需要维护data对象。

Vite作为构建工具比旧版Webpack在开发体验上强太多,冷启动快,热更新快。Element Plus提供了表格、表单、弹窗、消息提示等组件,后台管理页面几乎不用自己写UI样式,能集中精力处理业务逻辑。

这里补充一句:如果你对这个技术栈还不太熟,Vue的核心概念其实就三个——组件、响应式数据、路由。把这个三个概念理解透,前端开发就不会卡壳。我刚开始做这个项目时,第一个版本把大量逻辑堆在单文件组件里,后来才逐步拆分出独立的API模块、工具函数和子组件。

2.4 数据库选型:MySQL 8.0的具体注意点

数据库选了MySQL 8.0,原因是免费、稳定、社区资料多,而且8.0的JSON支持、窗口函数、utf8mb4字符集支持都比5.7更好,未来扩展数据分析类功能不用换库。

但MySQL 8.0有几个实际部署要注意的细节:

  • JDBC驱动要用com.mysql.cj.jdbc.Driver,不是老的com.mysql.jdbc.Driver
  • 连接URL建议显式指定serverTimezone=Asia/Shanghai,否则容易出现时区偏差
  • 建表时统一用utf8mb4字符集,菜名、用户昵称里生僻字才能正常存储
  • 如果服务器MySQL版本低于8.0,SQL语法和驱动配置会略有差异

这些看起来是小问题,但在真实部署中,十个项目有八个会在这里卡一下。后面部署章节我会再展开。

表格里我整理了一份我实际使用的环境版本,照着这个组合不会出现明显的兼容性问题:

组件版本说明
JDK1.8 / 11推荐JDK 11,长期支持稳定
Spring Boot2.7.x兼容JDK 8/11,生态成熟
MyBatis3.5.x配合mybatis-spring-boot-starter 2.x
MySQL8.0.x字符集utf8mb4
Vue3.4.x使用组合式API
Vite5.x构建速度快
Element Plus2.x后台管理UI
Nginx1.24.x前端静态资源托管与反向代理

这套组合在社区里的讨论量非常大,踩坑经验随手就能搜到。对个人项目或者中小型系统来说,性价比和可维护性都拉满了。

3. 从菜品表到订单表:数据库设计与后端接口的落地过程

3.1 数据模型设计:七张表撑起一套点餐业务

整个系统的数据模型我最终收敛为7张表。有些人会加地址表、评论表,我这里先不做,把核心链路跑通才是第一目标。

表名作用关键字段
user用户表username、password、nickname、role、avatar
category菜品分类表name、sort、status
dish菜品表category_id、name、description、price、stock、image、sales、status
banner轮播图表image、link_url、sort
cart购物车表user_id、dish_id、quantity
orders订单表order_no、user_id、total_amount、status、receiver、phone、address、remark
order_item订单明细表order_id、dish_id、dish_name、dish_image、price、quantity

几个设计细节值得单独说。

订单状态我定义了0待支付、1已付款、2已完成、3已取消。小餐馆线下收款,所以“待支付”实际上代表“已提交待商家确认”,“已付款”代表商家确认收款,“已完成”代表顾客就餐结束。状态用数字存储,展示层再映射成中文,好处是数据库里不会有字符串错别字问题。

金额字段全部用DECIMAL(10, 2),禁止用FLOAT和DOUBLE。餐饮金额涉及计算,浮点数在累加时会出现精度误差,0.1 + 0.2 这类问题在JavaScript和Java里都存在,DECIMAL从源头规避。

逻辑外键:订单明细里存了dish_name、dish_image、price的冗余快照。这是刻意的。因为菜品价格和名称会变,但订单历史必须保持下单那一刻的信息,关联查询时直接用快照字段展示,不需要再JOIN菜品表。这个设计在报表和订单详情场景非常实用。

库存字段放在dish.stock,下单时扣减。后面事务章节我会讲并发边界。

建表SQL太长,我只贴核心几条,完整SQL在部署资料里有:

CREATE TABLE `dish` ( `id` INT NOT NULL AUTO_INCREMENT, `category_id` INT NOT NULL, `name` VARCHAR(100) NOT NULL, `description` VARCHAR(500) DEFAULT '', `price` DECIMAL(10,2) NOT NULL, `stock` INT NOT NULL DEFAULT 0, `image` VARCHAR(255) DEFAULT '', `sales` INT NOT NULL DEFAULT 0, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表';

3.2 核心接口设计与调用关系

后端接口全部采用Restful风格,统一以/api开头。前端开发环境通过Vite代理转发,生产环境由Nginx反向代理,所以接口路径里不需要写服务器IP。

我列一下实际实现的接口清单,方便你对照自己的设计:

功能方法路径说明
注册POST/api/user/register用户名唯一性校验,密码BCrypt加密
登录POST/api/user/login返回token
获取用户信息GET/api/user/info根据token解析用户
分类列表GET/api/category/list状态为启用的分类
菜品分页查询GET/api/dish/page支持分类、关键词、价格区间
菜品详情GET/api/dish/{id}返回菜品完整信息
购物车列表GET/api/cart/list当前用户购物车
加入购物车POST/api/cart/add参数为dishId和quantity
修改购物车数量PUT/api/cart/update数量改为新值
删除购物车项DELETE/api/cart/{id}按购物车id删除
提交订单POST/api/order/create从购物车读取数据,生成订单
我的订单GET/api/order/list当前用户全部订单
后台菜品新增POST/api/admin/dish管理员角色
后台菜品修改PUT/api/admin/dish管理员角色
后台订单列表GET/api/admin/order/list管理员角色
后台订单状态更新PUT/api/admin/order/{id}/status管理员角色

登录鉴权我用的是轻量级方案:登录成功后生成随机token,存到Redis或者内存Map中,前端每次请求在Authorization头带上token,后端通过拦截器校验。如果你不想引入Redis,内存Map实现一个简单的TokenStore也完全够用。我项目里是这样设计拦截器的:

public class AuthInterceptor 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,解析用户信息放入ThreadLocal return true; } }

要注意的是,使用token方案时,服务端如果做了集群部署,内存存储会失效。这种情况要么引入Redis统一存token,要么改用JWT。单机部署场景内存方案没任何问题,我本地和生产测试机都跑得很稳。

3.3 MyBatis动态SQL:多条件菜品查询的实战写法

菜品查询是前端用得最多的接口。用户点“川菜”分类,同时搜索“麻婆豆腐”,还要求价格在50以内——这三个条件要能独立为空,也能任意组合。

写一个支持动态条件的查询,最优解就是MyBatis动态SQL。我在Mapper XML里是这样处理的:

<select id="selectDishPage" resultType="com.example.entity.Dish"> SELECT * FROM dish <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="maxPrice != null"> AND price &lt;= #{maxPrice} </if> AND status = 1 </where> ORDER BY sort DESC, id DESC LIMIT #{offset}, #{pageSize} </select>

<where>标签会自动处理AND前缀,条件缺失时不会生成多余的关键字。这里有个细节:字符串模糊匹配用CONCAT('%', #{keyword}, '%'),不要用${keyword}直接拼接,否则有SQL注入风险。LIMIT分页写死offset和pageSize,由调用方法传入。

还有一处我用foreach的地方是提交订单时批量插入明细:

<insert id="insertBatch"> INSERT INTO order_item (order_id, dish_id, dish_name, dish_image, price, quantity) VALUES <foreach collection="items" item="item" separator=","> (#{item.orderId}, #{item.dishId}, #{item.dishName}, #{item.dishImage}, #{item.price}, #{item.quantity}) </foreach> </insert>

批量插入比循环单条插入高效得多,一次网络往返搞定全部明细。

3.4 事务控制:下单扣库存不能“做一半”

订单提交是整个系统里最关键的事务。用户点“提交订单”按钮后,后端要做的事包括:查询购物车、校验菜品是否存在且上架、计算总价、生成订单主表、生成订单明细、扣减库存、清空购物车。

这七件事要么全成功,要么全失败。比如用户下单成功后扣库存的SQL执行错误,订单主表却已经写入,最终用户会觉得“我下单了,但商家没收到”,这是严重的数据不一致。

我的实现是在Service方法上加@Transactional,默认遇到RuntimeException回滚。扣库存的SQL用一条整的更新语句实现:

UPDATE dish SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{dishId} AND stock >= #{quantity}

WHERE stock >= #{quantity}是个隐性检查:如果库存不够,更新影响行数为0,程序检测后抛出异常,事务回滚。这一招比“先查库存再更新”更安全,因为数据库行锁保证同一时刻只有一个事务能修改这条记录,从源头避免了超卖。

再强调一点,内嵌Tomcat的连接池默认配置在并发高峰期可能不够用,我通常会在配置里把初始连接数和最大连接数显式设一下:

spring: datasource: url: jdbc:mysql://localhost:3306/food_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 20

4. Vue前端从零构建:页面、路由、请求封装与交互细节

4.1 前端工程结构与路由设计

前端我用Vue 3 + Vite + Element Plus创建工程。目录结构干净清晰,你拿到源码后能很快找到对应页面:

src/ ├── api/ # 所有接口请求封装 │ ├── dish.js │ ├── cart.js │ ├── order.js │ └── user.js ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── stores/ # 状态管理 ├── views/ # 页面组件 │ ├── home/ # 用户端首页 │ ├── dish/ # 菜品列表和详情 │ ├── cart/ # 购物车 │ ├── order/ # 订单 │ ├── user/ # 登录注册 │ └── admin/ # 后台管理 └── utils/ # 工具类

路由设计上分为用户端和后台管理两块。用户端路由包括首页、菜品列表、菜品详情、购物车、我的订单。后台管理路由包括菜品管理、分类管理、订单管理、轮播图管理。后台管理统一嵌套在AdminLayout布局组件下,实现左侧菜单+右侧内容区的经典后台布局。

路由守卫这块是重点。用户点“我的订单”、管理点“菜品管理”时,如果没登录或者角色不符,不能直接渲染页面。我在全局前置守卫里做了三件事:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (to.meta.requiresAuth && !token) { next('/login') return } if (to.meta.requiresAdmin && role !== '1') { next('/') return } next() })

这个守卫代码虽然简单,但能解决实际开发中90%的鉴权问题。前端守卫只能作为体验优化,真正安全的鉴权永远在后端,前端隐藏按钮、拦截路由,后端接口还是要二次校验管理员角色。

4.2 关键页面实现要点

首页是门面,我做了三个模块:顶部轮播图、分类导航、推荐菜品网格。轮播图数据来自/api/banner/list,菜品推荐直接取销量最高的前8个菜品。首页看起来简单,但数据加载时的loading状态必须处理,否则弱网环境下用户看到的是白屏。

菜品列表页接的是分页接口,默认每页12条。分类切换时会重新请求数据,这里有个容易踩的坑:切换分类时未重置页码,导致显示空白。我在分类切换事件里强制把currentPage重置为1再请求,这个问题就没了。

菜品详情页展示菜品图片、名称、价格、描述、库存。库存为0时,“加入购物车”按钮要禁用,否则用户加购成功后提交订单却提示库存不足,体验很差。

购物车页通过/api/cart/list获取数据,修改数量时调用更新接口,同时计算总价。这里我认为不应该在本地算总价,而是每次数量变化都重新从后端获取最新购物车数据,确保总价和服务端逻辑一致。虽然多一次请求,但避免了本地计算与后端价格不一致的隐患。

订单页显示订单号、下单时间、总价、状态、明细列表。状态值映射成文字和标签颜色,比如待支付显示黄色标签、已完成显示绿色标签。

4.3 axios封装与鉴权处理

axios封装是前端工程里必做的一件事。我建了一个统一实例,配置baseURL为/api,并设置请求拦截器和响应拦截器:

const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } else { ElMessage.error('网络错误,请稍后重试') } return Promise.reject(error) } )

响应拦截器里统一处理业务码和HTTP错误码,页面里只需要关心正常数据,错误提示全部在拦截层完成。这样做的收益是:几十个接口文件里不需要重复写错误处理逻辑,代码量缩减非常多。

4.4 前后端联调中的经典跨域问题

前后端分离开发时,前端跑在http://localhost:5173,后端跑在http://localhost:8080,直接调用接口会触发跨域。

跨域解决方案我建议开发环境用Vite代理,而不是在后端配全局CORS。因为代理对前端透明,代码里写/api短路径,生产环境由Nginx接替代理职能,前端代码完全不用改。

Vite配置如下:

export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

生产环境Nginx配置我会在第5章给出。这个方案有个好处:前后端部署在同一域下,不暴露后端端口,安全性也更好。

如果遇到请求能到后端但响应被浏览器拦截、OPTIONS预检请求报错的情况,这时才需要考虑在后端确实配置CORS:

registry.addMapping("/api/**") .allowedOrigins("http://localhost:5173") .allowedMethods("*") .allowCredentials(true);

但我要再次提醒,优先用代理方案,后端CORS只是兜底。生产环境如果你既开了代理又配了宽松CORS,有时候会出现重复头、跨域失效等诡异问题,排查起来很费时间。

5. 本地跑通再到服务器部署:一份能直接照做的上手指南

5.1 部署前置准备与后端打包

部署的目标环境是一台Linux服务器,我以Ubuntu/CentOS为例。需要安装的前置环境有:

  • JDK 11
  • MySQL 8.0
  • Nginx
  • Node.js(只在执行前端构建时需要,构建完可以不用装到服务器上)

回到本地,先处理数据库。把food_db.sql导入MySQL,注意建库语句和字符集,我使用如下语句:

mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS food_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p food_db < food_db.sql

后端工程在项目根目录下执行Maven打包:

mvn clean package -DskipTests

打包成功后,在target/目录会生成food-site.jar。这个jar包就是整个后端服务,包含内嵌Tomcat,直接运行即可:

java -jar food-site.jar --spring.profiles.active=prod

之所以用--spring.profiles.active=prod,是因为我在application-prod.yml里配置了生产环境的数据库地址和账号,避免把本地测试连接信息带到服务器。

5.2 前端构建与Nginx配置

前端构建在本地或者构建服务器上执行:

npm install npm run build

构建产物在dist/目录,里面是纯静态文件:HTML、CSS、JS、图片。把dist目录上传到服务器,比如/var/www/food-site。

Nginx配置是前后端分离部署的枢纽。我的配置思路是:静态文件直接由Nginx返回,/api/开头的请求反向代理到SpringBoot的8080端口,同时处理Vue Router的history模式刷新404问题。

server { listen 80; server_name your-domain.com; root /var/www/food-site; index index.html; # Vue history路由支持,刷新不404 location / { try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 图片等静态资源缓存 location ~* \.(png|jpg|jpeg|gif|svg|css|js)$ { expires 7d; add_header Cache-Control "public"; } }

这里try_files $uri $uri/ /index.html;是必写的。不写的话,前端路由跳到/dish/detail/1后刷新页面,Nginx会去找服务器上对应路径的文件,结果404。有了这行,Nginx会把所有不存在的路径请求回退到index.html,由Vue Router接管并渲染正确的页面。

后端服务我不建议直接用java -jar挂在终端里,终端一关服务就挂了。我用systemd管理:

[Unit] Description=Food Site Application After=network.target [Service] User=www WorkingDirectory=/opt/food-site ExecStart=/usr/bin/java -jar /opt/food-site/food-site.jar --spring.profiles.active=prod Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

配置好后systemctl enable food-site && systemctl start food-site,服务开机自启,进程崩溃自动拉起。

5.3 部署后的验证清单

部署完成不等于上线成功。我每次部署后都按固定顺序做一遍验证,你可以直接拿这个清单一键测完:

  1. 浏览器访问域名或IP,首页是否正常渲染
  2. 刷新菜品详情页,确认不出现404
  3. 注册一个新账号,确认能正常登录
  4. 在菜品列表页搜索“鱼香肉丝”,确认接口代理正常
  5. 加入购物车并提交订单,确认订单生成、库存扣减
  6. 用管理员账号登录,进入后台查看订单,确认角色权限生效
  7. 在后台修改一个菜品价格,回到前台确认价格更新

如果第3步之后任何一步卡住,先看Nginx错误日志/var/log/nginx/error.log和后端日志journalctl -u food-site -f。大部分问题的定位路径是:前端报错看Network面板 → 接口异常看后端日志 → 后端日志提示SQL错误看数据库配置。

6. 复盘:有几个坑提前知道能省半天时间

6.1 图片上传与静态资源映射的路径问题

开发时图片上传往往没问题,部署后图片却全挂了。原因出在SpringBoot默认不对本地磁盘任意路径提供静态访问。

我的方案是把图片保存到服务器指定目录(如/opt/food-site/upload/),然后通过WebMvcConfigurer添加资源映射:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:/opt/food-site/upload/"); } }

Nginx再对/upload/路径做一次alias映射或直接走静态资源,就能稳定访问。这里要注意,数据库里存的是相对路径/upload/xxx.jpg,不要存全路径带IP或域名,否则换服务器域名后所有图片链接都会失效。

6.2 订单并发与超卖问题

单机部署下,事务和行锁已经能解决大部分超卖问题。但如果你想把系统做得更健壮,建议加上“乐观锁版本号”机制:在菜品表增加version字段,更新时校验版本号一致才允许修改。

UPDATE dish SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{dishId} AND version = #{version}

不过对于小型餐饮场景,MySQL行锁 +stock >= quantity条件已经足够,不必过度设计。我在测试中模拟了100并发下单,没有出现库存负数的情况。真正要注意的是订单表索引,订单号务必加唯一索引,防止重复单号。

6.3 MyBatis驼峰映射和MySQL时区的一体化排查

如果查询返回的数据里,createTime这类驼峰字段映射到实体类时总是null,多半是MyBatis没开启驼峰映射。在配置文件里加上:

mybatis: configuration: map-underscore-to-camel-case: true

这是新手最容易忽略的配置。同样,MySQL连接URL里没有serverTimezone时,JDBC可能抛出The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized的异常,乱码一样的报错,其实只是时区识别问题。

6.4 敏感配置与安全加固

源码里我故意把数据库密码、Redis密码等敏感信息放在application-prod.yml中,不写进代码仓库;部署时用环境变量覆盖:

export DB_PASSWORD="你的数据库密码" java -jar food-site.jar --spring.datasource.password=$DB_PASSWORD

另一个容易被忽视的点是默认账号密码。管理员账号的初始密码一定要用BCrypt加密后再入库,不要为了图方便在数据库里存明文。登录功能我用的是BCryptPasswordEncoder,相同明文每次加密结果不同,即使数据库泄露,要还原出原始密码也很困难。

6.5 后续可以继续扩展的方向

这套系统跑通后,如果你还想继续丰富它,我建议按性价比排序:

  • 接入在线支付,打通“下单即付”闭环
  • 引入Redis缓存菜品列表和分类,降低MySQL压力
  • 使用对象存储托管图片,解决服务器磁盘扩容问题
  • 加入店铺信息管理、营业时间设置
  • 增加销量统计和菜品热度分析

我个人实际做下来的体会是:这种前后端分离的B/S系统,最难的不是某个单个功能,而是把“数据库表关联、事务边界、接口约定、前后端联调、部署链路”这一整条价值链理顺。这套美食网站系统我用同样的阅读顺序——先看表结构,再读接口,最后捋页面——能把所有代码读通。希望你照着上面这套路径去理解自己的项目时,也能少一点焦虑,多一点掌控感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询