☰
SpringBoot点餐前后台系统设计与实现
2026/10/20 15:24:06 网站建设 项目流程

这些年接了不少餐饮类的管理系统,也帮几个做快餐连锁的朋友搞过门店的数字化改造,前后折腾下来,最常被问到的一个需求就是:“能不能给我搞一个点餐系统,客人自己扫码点单,后厨和大堂能看到订单,最好后台还能管菜品、管库存、管报表?”说实话,这个需求听着不算复杂,但真正落地下来,涉及的点挺多的。

最近又帮某餐饮公司完成了一套基于SpringBoot的点餐前后台系统,从需求梳理、数据库设计、接口开发,到后台管理、前台点餐流程打通,再到部署上线,整个过程走了一遍,把一些通用性比较强的思路和踩坑记录整理出来。这篇就围绕这个项目标题展开,聊聊一个典型的SpringBoot点餐系统到底该怎么拆、怎么做、有哪些坑能提前躲开。

适合谁看?如果你正准备做毕业设计、接私活,或者想给自家小店搭一套线上点餐体系,又或者只是好奇这种系统内部是怎么运转的,这篇文章应该都能给你一点点参考价值。我会尽量说人话,把原理和实操都解释清楚,代码部分只给关键片段,不会贴一大堆让你看着就头疼的完整工程。

1. 项目整体设计与思路拆解

1.1 核心需求:点餐系统的边界到底在哪

点餐系统听起来简单,但“点餐”这两个字背后至少包含三个角色:顾客、商家(包括前台收银和后厨)、管理员。顾客的操作主要在手机端完成,比如扫码浏览菜单、把菜品加入购物车、下单、支付、查看订单状态;商家端要能看到新订单、接单、出餐、标记完成;后台管理则涉及菜品分类、菜品信息维护、库存管理、订单查询统计、账号权限管理。

如果只是做一个学生课程设计级别的Demo,可能只需要一张菜品表、一张订单表,再写几个增删改查接口就完事了。但真实业务场景下,这个系统会被拆成两套界面:一套给顾客用的前台H5点餐页面,一套给商家用的后台管理界面。两个界面共用同一个后端服务,SpringBoot就是承担这个服务端角色的。

从技术选型来看,SpringBoot解决的是后端接口开发和业务逻辑处理,前台点餐页面我选择了轻量的H5自适应页面,不需要单独开发APP,扫码就能访问;后台管理页面用了一套基于Thymeleaf服务端渲染的方案。前后端之间通过RESTful API通信,数据存储用MySQL数据库,再加一点缓存(比如Redis存验证码和热点菜品销量)来提升体验。

一开始接到需求时,需求方对“点餐系统”的预期其实很模糊:他们以为做个菜单展示和下单功能就行了。但真正梳理下来才发现,光是订单状态流转就有一堆细节:顾客下单后,订单可能要经过待支付、已支付、商家确认、制作中、配送中/待取餐、已完成、已取消、退款中等状态,每个状态谁可以变更、什么时候变更、变更后要通知谁,都需要在设计阶段定清楚。而且点餐系统往往不是孤立运作的,它要跟后厨打印、出餐大屏、库存联动这些外围系统对接。

1.2 模块划分:前后台分离但不分裂

一个比较合理的拆法是把系统分为三大部分:

模块面向用户核心功能典型页面/入口
前台点餐端顾客浏览菜单、菜品详情、购物车、下单、支付、订单状态扫码进入的H5页面
商家端店员/后厨/收银查看新订单、接单、出餐管理、桌台管理、订单打印平板或收银机上的商家操作界面
后台管理端管理员/老板菜品分类、菜品CRUD、库存、折扣、订单统计、员工账号电脑浏览器访问的后台管理界面

这三个模块不一定要做成三个SpringBoot项目,通常是一个SpringBoot工程内部按包结构划分,比如controller/customer、controller/admin、controller/merchant分别对应三类接口。但一定要确保权限隔离,不能让顾客随便调到管理接口。

有人可能会问:既然有后台管理,为什么还要单独搞一个“商家端”?其实很多中小餐厅的耐心收银员和后厨是没有时间接受复杂培训的,他们只需要一个简洁的操作面板:新订单提示、一键接单、标记出餐。后台管理的报表、菜品配置、员工管理是老板或店长用的,不应该跟日常操作混在一起。这个拆分在真实业务中非常关键,后期给需求方演示时他们也明显更认可这种分级设计。

1.3 技术选型背后的为什么

框架为什么要选SpringBoot?因为SpringBoot极大简化了Spring家族项目的搭建和配置,内嵌Tomcat,配置自动装配,依赖管理省心。配合Spring Data JPA或MyBatis操作数据库都非常成熟。针对点餐系统这种业务,JPA在处理简单关联关系时开发效率高,MyBatis在复杂统计查询时更灵活。我这次选的是Spring Boot 2.7 + MyBatis-Plus,原因很实际:点餐系统涉及大量联表查询和统计报表,MyBatis-Plus提供通用Mapper和条件构造器,写复杂SQL时也相对清楚。

前端H5用原生HTML + Vue 3(通过CDN引入)+ Element Plus的移动端组件?这里要注意,Element Plus更适合PC端,移动端着体验不好,所以前台点餐界面用Vue 3 + Vant组件库是常见组合。不过为了降低部署复杂度,我没有用独立的前后端分离工程,而是把H5页面和后台管理的静态资源交给SpringBoot的src/main/resources/static目录托管,这样只需要部署一个Jar包,同时解决了跨域问题。

数据库选型MySQL,这是最稳妥的方案。Redis我用来存顾客购物车缓存(未登录状态下购物车存在本地小程序的storage里其实也行,但如果要做同账号多设备同步,Redis更合适)和短信验证码。当然,短信服务在各类支付平台申请时需要资质,小店铺常直接跳过短信登录,用手机号+密码或者干脆微信一键登录。我这里做了手机号密码登录,押金支付方面接了支付平台的扫码支付沙箱环境做模拟。

从开发成本考虑,这个系统并没有引入微服务体系、Spring Cloud之类的重武器。一个单体SpringBoot应用足够撑起日单量几千的小型餐饮店,硬上微服务反而是给自己找麻烦。

2. 数据库设计:点餐系统的地基工程

2.1 表结构规划容易漏掉的几个表

点餐系统的核心表大家都能想到:菜品分类表、菜品表、订单表、订单明细表、用户表。但踩过几次坑之后,发现很多实际需求都得加表:

  • 桌台表:堂食扫码点餐时必须知道是哪一桌下的单。桌台表字段包括桌号、座位数、二维码标识(通常是桌台ID加密后的字符串或者短链)。
  • 购物车表:有些店铺允许顾客暂存购物车,下次再打开继续加菜。这个表可以基于用户维度做,也可以直接让前端存在本地,看需求。我这边选择了后端Redis缓存购物车,因为用户换了手机也能同步。
  • 菜品库存/每日限量表:很多店会有“今日特价菜每日限量50份”或“沽清”状态,需要每日可重置的库存计数。
  • 订单操作日志表:记录订单状态流转时间,比如下单时间、支付时间、接单时间、出餐时间,这个表对后期纠纷排查和效率优化非常有用。
  • 员工表/账号表:区分管理员、店长、收银员、后厨等角色,用RBAC权限设计。

2.2 表设计的关键字段与关系

我的核心表大致如下:

  1. category:id、名称、排序、是否上架、创建时间等。
  2. dish:id、category_id、名称、图片URL、描述、原价、现价、月销量、库存每日限额、是否推荐、是否上架。这里要注意价格建议用整数,以“分”为单位存储,避免浮点精度问题。
  3. member_user:id、手机号、密码(BCrypt加密)、昵称、头像、性别、注册时间、状态。
  4. cart:如果走Redis就无需建表,若基于DB则包括id、user_id、dish_id、quantity、selected(是否勾选)。
  5. orders(注意不要命名为order,因为order是SQL关键字):id、订单号、user_id、table_id、订单类型(堂食/外卖自取)、状态、总金额、支付方式、支付时间、备注、创建时间、接单时间、完成时间、收件人信息(若外卖)。
  6. order_detail:id、order_id、dish_id、名称快照、图片快照、价格快照、数量、小计。这里必须做“快照”,因为菜品价格和名称可能随时改,但历史订单要保留下单时的信息。
  7. stock_daily:id、dish_id、售卖日期、剩余数量。用于每日限量。
  8. order_log:id、order_id、操作人类型(用户/商家/系统)、操作内容、时间。

订单状态枚举非常重要,我定义在Java枚举里,并在数据库存整数,字段命名如status,可取值:0待支付、1已支付/待接单、2已接单/制作中、3已完成、4已取消、5退款中/已退款。不同业态状态略有出入,但整体差异不大。

2.3 两张容易忽略的索引优化

订单表按user_id和create_time建组合索引,因为用户端要查询“我的订单列表”,这个查询频率极高。orders表的order_no要建唯一索引,方便按订单号查询,同时订单号生成要保证唯一性,常用方案是时间戳+随机数,或者用雪花算法生成分布式ID。我这里偷懒用了yyyyMMddHHmmss + 随机四位数字,并发量不大时够用,但如果量大了还是建议走雪花ID,或者数据库自增主键只做物理主键,业务订单号另用全局序列生成。

另外,order_detail表一定要冗余菜品名称、图片、价格快照,不要在展示历史订单时再去join菜品表。这一点我在初版设计时没做,导致菜品改价后历史订单金额和菜品名全变了,后来花了很大精力修补,算是典型的“少建冗余字段引发的返工”。

3. 积分服务. 核心功能实现与登录认证

3.1 用户登录与权限控制的两种玩法

前台用户登录,后台管理员登录,本质都是认证+授权。对于单体SpringBoot应用,我推荐用JWT(JSON Web Token)来管理登录态,而不是传统的Session。原因很简单:H5前台和后端是同一域名下部署,但可能也有独立终端要调接口,JWT无状态、扩展性好,前后端分离也不容易被Session跨域问题卡住。

具体做法:用户传手机号+密码,后端校验通过后,生成一个JWT,里面包含用户ID和角色信息,设置过期时间(比如前台用户7天,后台管理员4小时)。返回给前端,前端存到localStorage,之后每次请求在Header上带Authorization: Bearer <token>。

后台管理系统我用Spring Security + JWT做的过滤器链拦截。说实话Spring Security的配置比较复杂,如果没有太严格的安全要求,也可以用拦截器(HandlerInterceptor)自己实现JWT解析和角色校验,代码量少很多,也更容易控制。我这次结合了Spring Security的UsernamePasswordAuthenticationFilter,因为后台管理涉及较细的权限划分(管理员、店长、收银员),Spring Security的注解@PreAuthorize用起来会很顺手。

给个简单的JWT工具类片段:

@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }

然后写一个拦截器,在配置类里注册,要排除登录接口、菜品查询接口以及静态资源路径。

3.2 扫码点餐流程完整链路

前台端最核心的链路是:扫桌台码 → 进入对应店铺的菜单页 → 加购 → 提交订单 → 支付 → 等待上菜。

桌台码我生成成二维码图片,内容是一个短链,比如https://domain/t/{tableCode},前端页面加载时会拿到tableCode,再请求后端接口GET /api/table/info?code=xxx来确定桌台号和店铺信息。这样做的用意是:不直接把桌号放URL里,而是用不可猜测的编码,防止用户改URL窜单。

菜单接口返回该店铺所有上架菜品,按分类分组。前端Vue页面拿到后用分类页签展示。菜品卡片显示图片、名称、月销量、价格,点击后弹出详情和可选规格(比如辣度、加料)。这部分规格功能我做成可选的,如果店铺菜品不需要,就不在后台配置。

购物车的数据结构是前端维护一个数组,每个元素是{dishId, name, price, count, specs},组件里实时计算总价。提交订单时,后端会重新计算一遍价格,而不是直接信任前端传的金额,这是必须做的安全措施。

提交订单接口做了几件事:创建订单记录(状态待支付)→ 扣减库存(这里要加锁或原子操作)→ 记录订单日志 → 返回订单号。库存操作要防超卖,SQL可以写UPDATE stock_daily SET remaining = remaining - #{count} WHERE dish_id=#{dishId} AND remaining >= #{count},用数据库行锁保证并发安全。

支付环节,为了演示和私活交付方便,我用的是模拟支付:提交订单后跳到一个支付确认页,点击“模拟支付成功”就调用后端/api/pay/callback,把订单标记为已支付并通知商家端(通过WebSocket推送新订单消息)。如果接真实支付,就用官方SDK,后端接收异步回调验签后更新订单,流程类似。

3.3 商家端接单与WebSocket实时推送

顾客支付成功后,商家端怎么第一时间知道?轮询接口是最简单但体验不佳的方式。我做了WebSocket推送:商家端登录后建立WebSocket连接,后端在支付回调成功时向对应店铺的商家推送一条“新订单待处理”的消息,商家端界面会弹提示音和红点。

SpringBoot集成WebSocket并不复杂,需要:

@ServerEndpoint("/ws/merchant/{shopId}") public class MerchantWebSocket { private static CopyOnWriteArraySet<MerchantWebSocket> sessions = new CopyOnWriteArraySet<>(); private Session session; @OnOpen public void onOpen(Session session, @PathParam("shopId") String shopId) { this.session = session; sessions.add(this); } public static void sendMessageToShop(String shopId, String message) { for (MerchantWebSocket item : sessions) { // 判断该session对应的shopId,一对一推送 } } }

这里要注意@ServerEndpoint创建的类是多例的,无法直接使用Spring的@Autowired注入Service,要么通过SpringContextUtil拿Bean,要么改成Spring管理的WebSocketHandler。我建议用Spring的TextWebSocketHandler实现,更利于业务层调用。

后厨出餐状态的变化同样通过WebSocket推送给前台用户,前端收到状态更新后自动刷新订单状态页。这部分的体验直接影响顾客对系统的信任感,“菜到底做了没有”是大家最关心的。

4. 后台管理功能:菜品、订单与统计报表

4.1 菜品管理模块:不只是增删改查

菜品管理看起来简单,无非是上传图片、填价格、选分类。但实际开发中几个细节值得提一下:图片上传要有大小限制、格式校验、压缩处理,保存路径不能暴露服务器物理路径,推荐保存相对路径(如/upload/202408/xxx.jpg),然后加一个虚拟映射。我用Nginx做了静态资源代理,后端只保存路径字符串,上传接口接收MultipartFile后存储到本地磁盘的/data/upload目录,并通过配置映射到URL。

菜品上下架要支持批量操作,还要配合每日库存。比如某套餐每日限量50份,后台可以直接调整当日剩余库存。月销量字段不能手工随便改,应该来自订单统计,或者通过定时任务每天从订单表汇总。我采用了后者:每天晚上凌晨统计前一天销量,更新菜品月销量,这样老板看到的数据才真实。

菜品排序很重要,菜单页的分类和菜品顺序直接影响顾客点单效率。排序字段我设计为Integer类型,后台可以通过拖拽保存顺序,也可以直接输入数字。

@TableName("dish") public class Dish { @TableId(type = IdType.AUTO) private Long id; private Long categoryId; private String name; private String image; private String description; private Integer originalPrice; // 分 private Integer sellingPrice; private Integer monthlySales; private Integer dailyStock; private Boolean recommend; private Boolean status; private Integer sort; }

4.2 订单管理:列表、筛选、详情、退款流程

后台订单列表要支持多条件筛选:订单状态、时间范围、订单号、手机号、桌台。分页查询用MyBatis-Plus的Page对象,条件构造器动态拼接搜索条件。导出Excel是老板很喜欢的功能,我用EasyExcel库做了订单导出,字段包括订单号、桌台、菜品、数量、金额、支付方式、状态、时间等。

退款流程比较麻烦。线下餐饮点餐常见的退款场景是:顾客下单支付后,商家还没接单,顾客申请退款,系统应自动退款;如果已经接单制作中,需要商家审核拒绝或同意。我这里设计成用户端可以发起退款申请,商家端看到申请后确认退款或者驳回,确认退款后调用支付平台的退款接口(沙箱环境模拟),然后订单状态变为已退款。整套流程会记录日志,避免扯皮。

4.3 统计报表:按小时、按菜品、按分类

统计报表是老板最容易看到“系统价值”的地方,哪怕功能再简单,一张柱状图也比表格有说服力。我做的统计主要包括:

  • 今日营业额(已支付订单合计)
  • 今日订单数、客单价、平均出餐时长
  • 近七天的日营业额趋势
  • 菜品销量Top10排行
  • 分类销售占比

实现方式用SQL聚合即可。比如近七天日营业额:

SELECT DATE(create_time) AS day, SUM(total_amount) AS amount FROM orders WHERE status IN (2,3) AND create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day;

图表我用ECharts渲染,后端提供JSON数据,前端调用接口后填充到图表中。这里的重点在于保证接口返回数据结构稳定,比如日期缺失的要补0,否则图表会出现断档。

5. 商家端实操过程:从需求到上线的关键步骤

5.1 搭建工程骨架

我用Spring Initializr生成基础工程,选择依赖:Web、MySQL驱动、MyBatis-Plus(自行引入)、Lombok、Validation、JWT工具包、WebSocket。配置文件里注意数据库连接参数、Redis参数、文件上传路径、JWT密钥等。密钥一定要强随机,不要明文写在代码里,可以放环境变量。

5.2 抽象通用返回类和异常处理

一个成熟的项目一定要有统一的返回结构,我定义Result<T>{code, message, data},code=200成功,非200失败。业务异常通过自定义BusinessException抛出,全局异常处理器捕获后返回给前端,避免错误信息直接把堆栈抛给用户。这样前端拦截器处理也方便:如果code==401就跳转登录,code==500弹提示。

5.3 前后台接口的路由划分

前台接口统一以/api/c开头,商家端接口以/api/m开头,后台管理接口以/api/a开头。配合对应的拦截器,权限一目了然。比如:

  • POST /api/c/auth/login
  • GET /api/c/dish/list
  • POST /api/c/order/submit
  • POST /api/c/order/pay
  • GET /api/m/order/pending
  • POST /api/m/order/accept
  • POST /api/a/dish/save

这样划分不仅安全,还便于写接口文档。我使用的是Springfox或Knife4j生成Swagger文档,接口排错和联调时效率提高不少。

5.4 前后台页面的快速搭建

为了让项目尽快跑起来,前台H5我花了比较多精力在移动端适配,因为扫码点餐要么用微信内部浏览器,要么用支付宝,所以视口设置、rem布局、安全区适配都要做。Vant组件品类很全,用它的NavBar、Card、SubmitBar就能搭出一个像样的点餐界面。

后台我用了基于Bootstrap或Element Plus的模板。毕竟是管理界面,操作密度高,表格、筛选、表单这些组件用起来顺手即可。Thymeleaf服务端渲染写后台虽然也能跑,但交互性较弱,我这次改成了前后端分离,后台管理是一个Vue工程,编译后放到SpringBoot的static目录里,统一打包成Jar。前端Vue工程可以用vite build把产物输出到src/main/resources/static,这样部署后就不用再单独起一个Nginx。这个做法很务实,适合单体应用,配合SpringBoot内嵌Tomcat直接对外服务。

5.5 部署上线中的环境配置

实际部署在阿里云或腾讯云的一台2核4G服务器上就够用,系统环境是CentOS +宝塔面板。后端打成的Jar包通过Systemd服务守护,MySQL和Redis用Docker容器跑,Nginx负责反向代理和HTTPS证书部署。反向代理主要为了统一端口和做域名绑定,前端H5和后台都走80/443端口,后端API走/api路径对外,Nginx转发到8080端口。

服务器上跑起来后,要记得开防火墙端口,配置数据库定期备份(mysqldump定时任务)。上线第一周我每天检查日志,用grep ERROR过滤异常,后来加了一个简单的日志可视化方案:用Logback将日志输出到/data/logs,配合docker log查看容器日志,出了问题能快速定位。

6. 常见问题与排查技巧实录

6.1 购物车数据与后端不一致

顾客在H5上加购时把价格存在了前端,提交订单时后端重新计算价格后发现与前端显示不一致。这种情况通常发生在后台修改了菜品售价,但顾客页面上还停留着旧价格。解决方法是:提交订单接口返回最新的菜品价格和总价,前端收到后弹出提示“部分菜品价格已更新”,请用户确认后再支付。如果用户不接受新价格,可以清空购物车。这是一种体验与数据一致性的妥协方案,总比用户支付了旧价格、店铺按新价格出餐引发纠纷好。

6.2 高并发场景下的库存超卖

人数多时比如秒杀限量菜,多个用户同时提交订单,如果扣库存SQL不是原子操作,很容易出现剩余库存变成负数。用UPDATE ... WHERE remaining >= #{count}保证只有库存够的时候才更新成功,影响行数为0则说明库存不足,直接返回“菜品已售罄”。此外,可以给扣库存加分布式锁(Redis setnx),但这里的场景用SQL条件更新已经足够,不必引入复杂锁。

6.3 订单状态错乱的幂等性处理

网络不好时用户重复点击“提交订单”按钮,可能创建多个相同内容的订单。前端要做按钮防重复提交(点击后置灰),后端也需要做幂等控制:前端生成一个唯一请求号(UUID),后端用Redis存该UUID作为key,处理过则直接返回上一次结果。我这次前后端都加了,实测下来基本杜绝了重复单的情况。

6.4 WebSocket推送不稳定

部署到云服务器后,WebSocket连接时不时断开,查出来是两个原因:Nginx没有配置WebSocket升级头(Upgrade和Connection),以及服务端设置了空闲超时。要解决,Nginx配置加上:

location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }

还要在SpringBoot里调整WebSocket session的idle timeout为0(表示不超时),或者设一个足够大的数值。

6.5 图片上传后页面无法访问

这是典型的静态资源映射问题。上传文件时保存到了本地/data/upload,但SpringBoot默认只处理classpath下的静态资源。需要配置一个虚拟映射,比如:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath + "/"); } }

注意Windows和Linux路径分隔符不同,建议统一使用Paths.get()处理。

6.6 前后端联调时跨域问题

如果前端页面和后端接口不在同一个域名下(比如前端Vite开发服务器在8081,后端在8080),就会出现跨域报错。开发环境可以配置Vite代理,把/api代理到8080端口,生产环境通过Nginx同域名反向代理也可以规避。我为了避免麻烦,直接把前端构建产物放进SpringBoot的静态资源目录,一个端口全搞定,但如果你习惯前后端分离开发,务必在SpringBoot中配CORS允许的域名白名单,不要用*。

几点个人体会

这套系统从开发到上线,最费心的地方不是那些花哨的前端效果,而是订单状态机、库存一致性和权限控制这些底层的逻辑。SpringBoot本身非常成熟,做业务系统时真正决定项目成败的是你有没有把“人”的角色想清楚:顾客要的是流畅,店员要的是少点几下,老板要的是能看到赚多少钱。你在设计每一个接口、每一张表之前,如果能先问一句“这个功能到底给谁带来什么价值”,方向基本不会偏。

最后再分享一个小技巧:点餐系统里所有金额相关的字段都用“分”存储,包括价格、金额、运费,展示时再除以100。一开始大家都会觉得麻烦,但久了你就发现,没有浮点数精度问题、没有银行四舍五入的争议,这个习惯能帮你省下好多不必要的麻烦。就算后面接支付平台的退款对账,也是一分钱误差都不出的。

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

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

立即咨询