“旧时光”这三个字听起来就很适合做项目背景——一家主打怀旧复古风的咖啡厅,既有堂食、外带,也有会员储值、积分兑换这些餐饮老玩法。技术栈选了Java + Spring Boot做后端,前端用Vue,这套组合在学校毕设、个人全栈作品、甚至小型创业项目里都非常成熟,资料多、社区活跃,遇到问题基本搜得到答案。
这篇文章我不会给你贴一层皮式的“项目展示”,而是按我实际做这类点餐收银系统的思路,从需求分析、表结构、后端接口、前端页面、怀旧主题落地到上线前容易踩的坑,完整走一遍。适合正准备做类似管理系统的同学参考,也适合已经写完业务、想优化代码细节的朋友当个检查清单。
1. 动手之前,先给咖啡厅管理系统“画像”
很多人做管理系统有个通病:拿到需求就建表,表建完就写增删改查,结果做到一半发现业务流程对不上,返工成本极高。“旧时光咖啡厅”这种项目,核心是运营闭环,第一步应该先把角色、场景和流程定死。
1.1 角色与使用场景
咖啡厅管理系统绝不是只有管理员一个角色的“单机软件”,我按照现场动线设计了四类使用者,分别对应不同的终端和界面:
| 角色 | 主要终端 | 职责 | 核心操作 |
|---|---|---|---|
| 管理员 | PC后台 | 门店全局管理 | 商品上下架、订单查询、数据报表、员工账号管理 |
| 收银员 | PC收银台/平板 | 堂食点单、结账、退款 | 快速开单、折扣、接单、打印小票 |
| 吧台/后厨 | 后厨显示屏 | 制作出品 | 查看待制作订单、标记完成 |
| 顾客 | 手机浏览器扫码 | 自助点餐 | 扫码看菜单、下单、查看订单进度 |
这里有个特别容易忽略的点:顾客端必须移动端优先,收银台要适应平板触屏操作,而后厨屏是大屏展示。一套 Vue 响应式布局,在桌面上做“宽度断点 + 触控按钮加大”就能覆盖,没必要像有些项目一样拆好几个独立前端工程。
1.2 核心业务流程梳理
我从“顾客进店到离店”这条主线倒推系统功能:
- 顾客到店,扫描桌台二维码;
- 手机端浏览菜单、加购、提交订单;
- 订单进入后台待支付状态,顾客在线支付或到收银台结账;
- 后厨屏显示新订单,按顺序制作,点击完成;
- 收银台打印小票,顾客取餐,或服务员送餐;
- 结账后会员积分自动累计;餐后顾客可查看历史订单。
这一套流程对应到后端就是:桌台模块、菜品模块、购物车模块、订单模块、支付模块、后厨状态流转模块、会员模块。你的“管理后台”其实只服务其中三分之一的需求,另外三分之二都在给顾客和后厨做工具。
当时我画完流程有个很关键的体会:不要把一个需求的每一步都做成页面。比如“后厨屏”根本没有复杂表单,就是一张轮询更新的列表加一个完成按钮;真正重的逻辑都在订单状态的流转和并发控制上。
2. 数据库设计:“旧时光”的家底怎么盘
表结构设计是这类系统的地基。地基歪了,后端的业务代码写得再漂亮也是空中楼阁。
2.1 核心表清单
我按业务域拆成六大模块,自认为这是咖啡厅管理系统性价比最高的建模方式:
- 用户与权限:user(员工/管理员)、role、permission,或者直接用更简单的 user + role 两张表 + 前端按钮级权限判断。多数校园项目用 JWT + 拦截器判断角色就够,不需要引入复杂的权限框架。
- 菜品域:category(菜品分类)、product(菜品)、product_spec(规格,比如大杯/中杯、常温/加冰),还有 product_image 处理多图切换。
- 桌台域:table,包含桌号、座位数、当前状态(空闲/占用/待清理)、绑定的二维码。
- 订单域:orders(主表)、order_item(明细表)、order_status_log(状态流转记录)。
- 会员域:member(会员档案)、member_balance_log(储值流水)、member_points_log(积分流水)、coupon(优惠券)和 user_coupon(领券记录)。
- 仓储域:material(原材料)、purchase(进货单)、stock_record(库存流水)。咖啡厅也是要算物料成本的,这一块做好了,管报表时才有东西可看。
有些同学对“状态机日志表”不重视,觉得多写一张表很麻烦。真上线后你会发现,顾客说“我明明支付了但显示未支付”,或者后厨说“这单怎么重复出现了”,查订单状态历史时没有日志,你只能跟用户来回拉扯。加一张 status_log,每次状态变更记一下谁改的、从哪改到哪、为什么改,排查问题效率翻倍。
2.2 建表时的几个细节
订单主表我建议长这样(省略部分字段):
CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '订单号', table_id BIGINT COMMENT '桌台ID,自取可空', member_id BIGINT COMMENT '会员ID,非会员可空', total_amount DECIMAL(10,2) NOT NULL COMMENT '应收金额', discount_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '优惠金额', pay_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额', pay_type TINYINT COMMENT '1微信 2支付宝 3现金 4储值卡', status TINYINT NOT NULL COMMENT '订单状态', remark VARCHAR(255) COMMENT '顾客备注', create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, KEY idx_order_no (order_no), KEY idx_table_status (table_id, status), KEY idx_create_time (create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';几个容易踩的点:
- 金额一律
DECIMAL(10,2),Java 里对应BigDecimal,绝不用double。咖啡单价看似是 18 元、28 元这种整数,但折扣、满减、积分抵扣算下来,浮点数误差积累几万单之后会很恶心。 - 订单号
order_no我习惯由后端生成,用时间戳 + 随机序列 + 门店前缀,比如2025031215301001。不要用自增 ID 当订单号漏给顾客,既不安全也难辨识。 - 所有跟“状态”有关的字段,建议用
TINYINT数字而不是字符串,表里注释写明每个数字的含义。Java 侧用枚举类映射,前后端统一状态码字典。 - 凡是需要列表查询的表,都给 create_time 和状态字段建索引。管理端“查今天的订单”“查某桌待支付订单”是最常见的高频查询,没索引的话数据量过万就会开始卡。
2.3 索引与查询优化
后台报表页经常出现“时间范围 + 状态 + 门店”组合查询,这种场景别想着一个复合索引通吃。我当时的做法是:订单主表给(status, create_time)建联合索引,因为最常出现的筛选是“某状态 + 最近时间”;桌台表按(store_id, status)走;订单明细表则完全靠order_id查询,只建单列索引。
运行时如果发现某个报表接口慢,用EXPLAIN看一下是否走了索引,别盲目加索引——咖啡厅系统写入频率远高于读取,索引过多会导致插入和更新变慢,得不偿失。
3. Spring Boot 后端:把“重复劳动”砍到最低
后端我用的版本组合是 Spring Boot 3.2.x + JDK 17 + MyBatis-Plus + MySQL 8。选这个组合的理由很简单:Spring Boot 3 已经非常稳定,JDK 17 是长期支持版,MyBatis-Plus 做单表 CRUD 几乎不用写 SQL,可以把精力集中在真正复杂的订单流转和报表聚合上。
3.1 工程结构不要过度设计
现在网上很多项目一上来就拆 Maven 多模块:common、system、order、member……对于一个单体就能扛住的咖啡厅管理系统,我强烈建议用单模块,按包名分域,目录长这样:
com.oldtime.cafe ├── config # 全局配置:CORS、MyBatisPlus、Jackson ├── common # 统一返回体、异常、工具类 ├── security # JWT 认证与拦截器 ├── controller # 接口层 ├── service # 业务层 ├── mapper # MyBatis-Plus Mapper ├── entity # 数据库实体 ├── dto # 请求/响应对象 └── vo # 前端视图对象只要你的代码不是烂到完全不可维护,这种包结构足够支撑到几千家门店的规模。多模块真正的收益是在团队协作和强制依赖边界上,一个人开发的项目用多模块纯属给自己找编译麻烦。
3.2 认证方案:JWT + 拦截器
咖啡厅的角色不多,用 Spring Security 也绰绰有余,但我实际用的是轻量方案:登录接口发放 JWT,前端每次请求带上 Token,后端写一个拦截器解析 Token、校验角色、放行请求。
JWT 的“无状态”特性在这个场景很合适:顾客手机端、收银台 PC、后厨屏三个终端共享同一套认证,不需要服务端维护 Session。但要注意 JWT 过期时间的设置——顾客点餐可能长时间不退出,我设置的是 7 天;员工后台则设置 2 小时,过期后重新登录。
登录接口的核心逻辑大致如下:
@Override public LoginResult login(String username, String password) { User user = userMapper.selectOne( new LambdaQueryWrapper<User>() .eq(User::getUsername, username) ); if (user == null) { throw new BusinessException("用户名或密码错误"); } if (!passwordEncoder.matches(password, user.getPassword())) { throw new BusinessException("用户名或密码错误"); } if (user.getStatus() != 1) { throw new BusinessException("账号已被禁用"); } String token = jwtUtil.generateToken(user.getId(), user.getRole(), user.getNickname()); return new LoginResult(token, user.getRole(), user.getNickname()); }有一件事必须单独提醒:顾客扫码点餐本质上也是登录。不要让顾客去注册账号,注册门槛会把手机点餐转化率砍掉一半。顾客端用“微信授权 + 手机号”方式,或者干脆生成一个临时游客身份,结账时才引导绑定会员。我在表设计里的 member 就是可空字段,原因就在这里。
3.3 订单提交接口与并发思考
点餐系统最容易出事故的就是“提交订单”这个动作。顾客瞬间提交、多次点击、库存修改、余额扣减,任何一个环节不加控制都会出问题。
我的订单提交流程分四步:
- 幂等校验:前端生成“请求唯一标识”(比如 UUID),提交时带上。后端用 Redis 的 SETNX 缓存这个标识,如果 3 秒内重复提交直接返回“订单处理中”。这一步能挡住 90% 的重复点击问题。
- 库存预扣:读取商品库存,预扣除购买数量,而不是等到支付完成才扣。如果顾客 15 分钟未支付,库存自动回补。
- 计算金额:加购项单价 × 数量,减去优惠券金额,再算会员折扣,全程用
BigDecimal。 - 事务落库:在事务里写入订单主表、明细表、库存流水、优惠券核销记录。用
@Transactional(rollbackFor = Exception.class),明确指定回滚条件。
我当时踩过一个大坑:订单超时未支付回补库存用的是定时任务扫表,结果高峰期定时任务 3 分钟跑一次,顾客看到“还有 5 件”点进去却发现没有了。后来改成 Redis 延迟队列 + 订单状态扫描双保险,体验才真正达标。
3.4 后端基建:统一返回、全局异常、日志
接口返回格式一定要统一,这是前后端协作的底线:
{ "code": 200, "message": "success", "data": {} }为了少写重复代码,我做了一个ApiResponse<T>泛型工具类,配合全局异常处理器@RestControllerAdvice,这样业务里想报错时直接throw new BusinessException("库存不足"),前端拿到的永远是同一个结构。日志方面,用 Slf4j 在关键节点打日志:登录、下单、支付回调、退款、库存变更。上线后顾客来反馈问题,你只需要搜订单号就能串起整个过程。
4. Vue 前端:从路由守卫到扫码点餐
前端我用的是 Vue 3 + Vite + Element Plus + Pinia + Axios,这套组合是目前 Vue 生态的主流答案。Vite 比 Webpack 快的不是一点点,开发体验舒服太多了。
4.1 工程目录与 API 封装
Vue 前端单仓库多页面即可,按“后台管理”和“顾客端 H5”拆成两个路由空间,但共用一套请求封装和组件:
src ├── api # 所有接口定义,按模块拆分 ├── assets # 静态资源、怀旧主题图片 ├── components # 通用组件 ├── layout # 后台布局 ├── router # 路由配置 ├── store # Pinia 状态 ├── views │ ├── admin # 管理后台页面 │ └── mobile # 顾客扫码点餐页面 └── utils # axios、工具函数Axios 封装里最核心的是拦截器。请求拦截器从 Pinia 的 token 状态里取 JWT 塞进 Header;响应拦截器统一处理业务错误码,401 跳登录,500 弹出错误提示。这段代码每个项目都得写一遍,但写好之后所有页面调用接口就是一行await api.order.submit(params),干净利落。
4.2 路由守卫与角色控制
管理后台的访问控制,说复杂也复杂,说简单也简单。我用的是“路由 meta + 全局前置守卫”:
router.beforeEach((to, from, next) => { const store = useUserStore(); if (to.meta.public) { next(); return; } if (!store.token) { next('/login'); return; } if (to.meta.roles && !to.meta.roles.includes(store.role)) { next('/403'); return; } next(); });把“哪些页面哪些角色能进”写在路由表的 meta 里,加页面时顺手声明权限,维护成本非常低。管理员能看到报表和员工管理,收银员看不到这些入口,后厨屏甚至不需要登录——它是独立路由,走的是店内局域网接口。
4.3 扫码点餐的实现思路
顾客端最核心的交互是“扫码 → 看菜单 → 加购 → 下单”。二维码不是技术难点,但有几个体验细节值得说:
- 二维码内容用一个短标识符,比如
cafe://table/12,而不是完整 URL。这样换个域名二维码不用重新生成。 - 扫码后先调接口查询桌台状态,如果已占用则提示“本桌已开台”,避免重复开台。
- 加购按钮要大,触控区域不小于 44px;菜单分类左右布局,右侧菜品列表可滚动。
- 购物车做成悬浮底部栏,点击展开明细,整个交互参考外卖 App 但比外卖更简单——不需要配送地址,省掉一大块表单。
购物车状态我用 Pinia 管理,刷新页面会丢失,所以下单前用户看到的购物车是前端本地状态。这里有个小优化:把未提交的购物车持久化到 localStorage,顾客手滑刷新页面购物车还在,体验会好很多。
4.4 后厨屏:最简单的界面,最需要注意的刷新策略
后厨屏不是一个 CRUD 页面,而是一个“实时看板”。我用了两种刷新方式组合:页面加载时拉一次全量待制作订单;之后通过 WebSocket 推送新订单事件,收到事件后再拉增量列表。用原生 WebSocket 就行,不需要引入 Socket.IO。
这里有个小坑:如果咖啡馆网络不稳定,WebSocket 掉线期间新订单会堆积。解决方案是前端加一个 30 秒的轮询兜底,发现 WebSocket 状态异常就自动切换到轮询模式,页面顶部显示“连接已降级,每 30 秒自动刷新”。
5. “旧时光”视觉主题:怀旧不是放几张老照片
这个项目的名字本身就是最大卖点,界面风格如果做成默认蓝白后台,那“旧时光”三个字就白起了。
5.1 设计关键词拆解
我把“旧时光”拆成三个视觉关键词:暖木色、纸张质感、复古字体。
主色调不用多,定一个深咖色#4A3426作为品牌主色,辅以奶油底色#F5EFE0和点缀的怀旧红#A65B3C。Element Plus 的主题变量可以自定义,把这个几个颜色替换进去,整个后台的按钮、输入框、卡片会自动全部换肤,比手工改 CSS 划算得多。
5.2 几个低成本高质感的落地技巧
- 菜单卡片模拟老菜单纸张:背景用奶油色渐变加极淡的噪点纹理,字体用手写风格的标题字体(中文可以找免费商用字库),价格用等宽数字字体。
- 收银台界面做“胶片感”:页面切换用平滑淡入而不是生硬的跳转,小图标统一使用线性风格,整体降低饱和度和对比度。
- 首页放“记忆墙”:顾客端 H5 的首页不是上来就逼你下单,而是先放一组品牌故事照片和新品海报,往下滑才是菜单。这个设计让顾客愿意在页面上多停留一会儿,间接提升点单概率。
- 小票样式复古化:订单完成后的“电子小票”做成带锯齿边缘的卡片,字体用像素感和圆角感兼顾的中文字体,顾客截图分享本身就是免费宣传。
怀旧不等于放弃现代体验。所有按钮至少 44px 高,字号不小于 14px,关键操作有明确的按下反馈。视觉风格再旧,交互逻辑必须新。
5.3 响应式打印适配
收银小票是 Java + Vue 项目里容易被忽略的环节。小票打印一般走浏览器打印,但页面宽度、字体大小要单独适配 58mm/80mm 热敏纸。我的方案是专门做一个打印组件,生成纯表格样式的小票页面,再用@media print隐藏导航和按钮,只保留小票区域。这里有个实用技巧:小票宽度设成 58mm,@page { size: 58mm auto; margin: 0; },打印时选择“实际大小”,不要“适应页面”,否则字体会被压缩到看不清。
6. 从开发到上线:实测中我反复踩的坑
功能写完只是第一步,能稳定跑起来才是真本事。整理几个我在这个项目里反复踩、也帮别人排查过的问题。
6.1 金额和时间:两个“差之毫厘谬以千里”的细节
金额精度问题前面提过,这里再说一个隐蔽场景:订单金额在数据库是DECIMAL(10,2),MyBatis-Plus 映射到 Java 会给你BigDecimal,没问题。但前端 JavaScript 计算金额时,0.1 + 0.2 这种浮点误差会直接爆出来。前端只负责展示,金额计算一律以后端返回为准,前端不要做任何加减乘除。购物车页面的“合计”必须是后端在加购时就算好返回的,而不是前端把单价相加。
时间问题则是时区加格式的组合拳。后端用LocalDateTime统一存数据库,接口返回格式统一成yyyy-MM-dd HH:mm:ss。Jackson 配置里要显式设置日期格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8不然前端拿到的是数组或者带 T 的 ISO 格式,解析起来一脸懵。另外数据库连接串记得加serverTimezone=Asia/Shanghai,这个坑在部署到云服务器后最容易出现——本地好好的,服务器一跑时间全错。
6.2 商品图片上传:本地存储还是对象存储?
如果只是校园项目或者单店部署,最稳妥的方案是本机磁盘存储 + Nginx 映射静态资源。图片上传接口把文件存到服务器某个目录,返回给前端一个/images/products/xxx.jpg路径,由 Nginx 直接服务,后端完全不用管读取。
但如果你打算做成可商用、支持多门店的系统,就得上 MinIO——它是一种兼容 S3 协议的开源对象存储。商品图片、品牌素材全部打到 MinIO 桶里,加个桶权限策略,图片链接走 Nginx 代理缓存,会比自己管理磁盘干净很多。不管是本机存储还是 MinIO,上传接口统一返回相对路径,不要返回完整 URL,这样以后换域名、换 HTTPS 都不用改数据库里的旧数据。
6.3 商品上下架与缓存一致性
菜单是高频读取、低频修改的数据。顾客端每次打开都查 MySQL 显然不划算,我在 Redis 里存了一份“在售商品列表”,key 为menu:store:{storeId},30 分钟过期。
这里真正要注意的是缓存更新时机:商品上架/下架、改价格、改库存这些操作发生时,必须主动删除缓存,而不是依赖过期。因为我用的是“被动失效 + 主动删除”策略:管理员修改商品后立刻删缓存,下一次查询重新从数据库加载。极端情况下缓存和数据库最多有几秒不一致,这点延迟对咖啡厅场景完全可接受。
6.4 报表导出:Java POI 的真实边际
管理后台通常会有“营业报表导出”需求,常见实现是 Apache POI 生成 Excel。很多同学一听说 POI 就头疼,其实餐厅报表用最基本的XSSFWorkbook就够了,做一个订单汇总表:
try (XSSFWorkbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("营业报表"); Row header = sheet.createRow(0); header.createCell(0).setCellValue("日期"); header.createCell(1).setCellValue("订单数"); header.createCell(2).setCellValue("营业额"); // 遍历订单数据填充... ByteArrayOutputStream out = new ByteArrayOutputStream(); workbook.write(out); return out.toByteArray(); }POI 完全支持在 Excel 里生成图表,比如月度营业额柱状图。如果你只是导出报表,不需要整那些花活,老老实实把数据铺出来就行;图表展示交给前端用 ECharts 渲染,性能和美观度都远超 Excel 自带图。
6.5 Spring Boot 版本与依赖冲突
用 Spring Boot 3 记得 JDK 必须是 17 及以上,MyBatis-Plus 也要用适配 Spring Boot 3 的版本(mybatis-plus-spring-boot3-starter)。很多同学项目启动报一堆错,十有八九是 Spring Boot 2 的依赖习惯性复制到了 3 里。还有 Lombok 插件版本也要同步升级,旧版 Lombok 和 JDK 17 组合经常出现java.lang.ClassFormatError。
我的建议是:新项目直接 Spring Boot 3 + JDK 17,不要犹豫。网上虽然 Spring Boot 2 教程多,但类名和方法基本一致,遇到差异时翻官方文档比查旧博客靠谱得多。
7. 我想额外说给“做毕设”的人听的几句话
这套系统作为毕业设计,技术栈非常合适,因为它的技术覆盖面广:Spring Boot 的接口开发、MyBatis-Plus 的数据操作、JWT 认证、Vue 的组件化和路由权限、数据库设计、甚至桌台二维码这种 IoT 边缘场景都能讲出东西来。但切记——不要堆砌没有业务逻辑的 CRUD 页面。
提升项目含金量的方向不是增加页面数量,而是把几个核心场景做深:
- 订单并发控制做了没有?
- 金额计算是否全程可靠?
- 状态机流转是否清晰可追踪?
- 前端交互是否有思考过真实用户的使用感受?
- 有没有报表分析、缓存优化这些“非必选但加分”的能力?
答辩时能把这几个问题说到位,比列十个“增删改查模块”管用得多。
咖啡厅管理系统这类项目最迷人的地方在于,它离真实生活很近。你把代码写完、部署上线,看着顾客用手机扫码下单、收银台打出复古小票、后厨屏弹出新订单,那种“技术真的在服务具体的人”的感觉,会让人觉得这段代码写得值。如果这个项目能帮到你,哪怕只是其中一个建表思路、一个排查方向,我就觉得这篇分享没白写。祝你开发顺利,记得给自己留一杯咖啡的时间。