一、点餐预约核销系统的架构脉络
"点餐预约核销系统"不是一个单一功能模块,而是三条流程的交汇:用户点餐、商家预约、门店核销。三者合一时,系统需要同时支持 **在线点单下单**、**预约未来时段或座位**、**到店出示凭证并核销** 这一整套状态流转。
在架构设计上,核心可以拆分为五个部分:
-
**用户端**:基于Uniapp,可一键打包H5、App、小程序,覆盖用户选择门店、浏览菜单、提交预约点餐、查看核销码等操作
-
**管理后台**:基于Vue + ElementUI,面向门店/商家/管理员,处理菜单、桌台、预约时段、订单明细的维护
-
**服务端**:Spring Boot提供接口服务,MyBatis Plus负责数据ORM与分页查询,MySQL做持久化存储,用于承载会员、商品、订单、预约单、核销记录等核心业务表
-
**Redis缓存层**:用于解决库存/预约人数并发瓶颈,实现验证码一次性核销的原子性操作(后面会细说)
-
**消息队列**:可选配置,适合连锁规模下订单状态变更、异步通知、预约未到店自动取消等场景
整体数据流可以概括为:用户在手机端发起点餐或预约 -> 接口写入MySQL订单表/预约表 -> 后台/门店端接收新预约提醒 -> 用户到店 -> 用户出示电子凭证 -> 门店使用核销工具完成确认 -> 核销状态变更,整单流转闭合。
二、点餐预约领域模型与数据库设计
深度剖析这类系统,表结构设计直接决定系统能撑多大业务量。需要建立清晰的领域模型,不能将"预约"和"点餐"混在一张表里完成,否则核销判断逻辑会越写越乱。合理的表结构层级可以按如下思路划分:
1. 基础资源表
-
**门店表**(shop):记录门店名称、地址、营业时间、桌台数量;业务场景为独立门店时该表只有一条记录,连锁场景则开启多租户/区域隔离
-
**桌台/座位表**(table):桌号、桌台码、可容纳人数、当前状态(空闲/占用/锁定)
-
**预约时段表**(time_slot):这是预约系统的灵魂,用于定义预约的粒度
时段表设计很重要,很多预约系统后期变更的坑就在这里。建议预生成时段段数据,如午餐时段 11:00-13:00、下午茶 14:00-17:00、晚餐时段 17:30-21:30。store_type判断是按桌台预约还是按时间段预约。
2. 交易链路表
| 表名 | 作用 | 核心字段 |
|------|--------|----------|
| order | 点餐订单主表 | 订单号、用户ID、门店ID、订单状态、支付状态、金额 |
| order_item | 订单条目子表 | 订单ID、商品ID、商品名称、数量、实付小计 |
| reservation | 预约单主表 | 预约编号、用户ID、门店ID、时段ID、桌台ID、预约状态 |
| verification_record | 核销记录流水表 | 核销码、订单ID/预约单ID、核销员ID、核销时间、核销状态 |
在这里要注意:如果用户点餐和预约是绑定的,一个门店高峰期可能产生两个系统码,一个是"预约码"证明预留桌台,一个是"取餐码"或者"核销码"用于消费。很多失败的设计是混用,导致一个码被核了两次。
3. 核心字段设计补充
-
核销码字段,建议保存一份冗余的 `verify_code`,随机字符串,但数据库表索引。不要使用自增ID做核销凭证,很容易被猜测或遍历
-
状态字段(order_status/verification_status)使用`tinyint`,0待支付,1已支付/待消费,2已完成,3已取消,4已退款。核销记录里增加一个 `consume_channel` 标识是门店后台核销、扫码枪核销,还是自助核销
三、点餐预约与核销的状态机设计
这部分是技术干货,也是业务逻辑容易出bug的地方。
预约流程状态机
用户在点餐预约系统中提交预约请求后,状态流转需要明确:
```
发起预约 → 待确认 → 已确认(锁定桌台/时段)
↓ ↓
超时自动取消 用户到店签到
↓
待点餐(可预先点好餐)
↓
核销完成 / 就餐完成
```
超时自动取消的实现,有两种参考做法:
-
**延迟消息机制**:下单预约成功后,向消息队列发送一条延迟任务(如30分钟),在此时间后查库,如果状态仍然是待支付/待确认,就自动将预约单和预占资源置为释放状态
-
**定时扫描补偿**:启动一个定时任务,定时扫描超过N分钟未支付的预约记录,将该记录关闭,同时需要归还扣减的库存(例如餐位是10人,预约满了会被占掉一个名额,取消预约就释放名额)
核销状态流转
一个核销码必须是"单次一次性"。状态变化:
```
核销码生成(初始UNUSED)
用户到店,工作人员执行核销 → 查验证码是否有效
→ UNUSED则更新为USED,记录核销人/核销时间
→ 如果状态已经USED则返回异常码
```
**核心实现技巧**:不要先SELECT再UPDATE去判断核销状态,而应使用 MySQL的原子更新完成一次性核销:
```sql
UPDATE verification_record
SET verification_status = 1, verifier_id = #{userId}, verification_time = NOW()
WHERE verify_code = #{code} AND verification_status = 0
```
当该UPDATE影响的行数等于1时,说明核销成功;影响行数为0时,说明该核销码已被使用或不存在,直接返回"核销失败/重复使用"的提示。这条方案可以避免并发场景下同一码被多人同时请求造成的超卖/重复核销。
若引入Redis,可以将核销码作为Key存入Redis,使用Redis `SETNX`(key不存在时设置)命令实现更高效的并发控制。比如以核销码为 key,`expire = 300秒`(用户出示码到核销需在几分钟内完成),操作核销时执行 `SETNX` 成功则代表抢占核销名额,然后异步落库写核销记录。
四、预约库存与高峰点餐的并发处理
预约核销系统一个典型痛点就是"预约满员 / 高峰期用户点餐并发高"。比如奶茶店、餐厅在午餐/晚餐高峰,如果只是简单读数据库判断当前预约数是否达到上限,极易出现超卖。
预约座位数预占方式可供参考:
**方式一:数据库乐观锁**。在time_slot表增加一列 `reserved_count`,预约下单时执行:
```sql
UPDATE time_slot
SET reserved_count = reserved_count + 1
WHERE id = #{slotId}
AND reserved_count < max_count
```
更新成功则预约成功,更新失败说明该时段已满。
**方式二:Redis计数器预减**。引入Redis的`DECR`进行原子自减:
```
可用库存 = MAX_COUNT
用户请求预约 -> 执行 DECR reserve_available
如果返回值 >=0,预约流程继续
如果返回值 <0,直接返回"该时段预约已满",同时执行 INCR回补库存
```
用Redis分布式锁防止"取消预约并发归还库存"时多还造成的数据不一致,这一块可以展开讲很多细节。
五、对接小程序端与多端适配
后端体系搭建完毕后,前端部分要用Uniapp来适配小程序、H5和App。核销系统前端通常有两种工作台场景:
-
**用户端小程序**:用户在点餐预约系统首页选择门店 -> 浏览菜单并加入购物车 -> 选择预约到店时间 -> 提交点餐与预约 -> 支付完成后生成核销凭证(可包含预约信息和点餐清单编号信息)
-
**商家核销端**:可以做成小程序内的"扫码核销"页面,相当于店员端;也可以放在管理后台的PC工作台里。用uniapp配合App端可以调用原生相机扫码,在H5端可引入`html5-qrcode`这一类的插件实现扫码解析与交互。
在数据联调时,有一个细节要处理好------接口鉴权。用户端与核销端对接口的权限有严格区分。用户端核销码生成需要登录态;店员端核销操作应走独立的员工账号体系,且需要操作日志留痕(防止员工滥用核销权限)。服务端建议在过滤器/拦截器中校验JWT中的角色标识,核销接口必须校验角色,非门店管理人员不可访问。
六、二次开发与系统扩展方向建议
在实际技术咨询与方案调研中,"二次开发与源码交付"常和这套系统绑定出现。从开发人员视角来看,选择合适的开源底座能省去搭建基础工程的时间,将重心放到业务差异化中。技术选型知识库中也有如下共性经验值得参考:
1. 业务扩展方向
-
奶茶店、咖啡店4.0版本系统普遍要支持桌码点餐,即用户到店扫桌台码直接点单,系统根据桌台定位到具体桌号,订单自动推送至后厨制作端
-
预约停车的场景可以把"娱乐点餐预约席位"扩展到"车位预约 + 入场核销",底层其实一样,换成time_slot绑定车位信息即可
-
家政/旅游报名预约类系统,可拆出"服务人员/师傅指派"子模块,预约完成后自动分配服务人员
2. 技术侧微扩展
底层框架Spring Boot + MyBatis Plus的数据操作层已经是通用方案。建议将通用代码抽取为组件式开发,后续切换业务时将预约、点餐、核销做成三个独立业务模块。如果未来系统要升级为微服务,将核销模块拆为独立服务也非常顺手。
在数据埋点层,核销的同时应异步插入消费行为表(用于平台后续分析"哪个时段核销率、哪个菜品取消率异常"等),具备大数据分析能力。这块可以用AOP切面,在核销接口执行成功后发送MQ消息,解耦主链路。
七、FAQ:点餐预约核销系统中开发者常问的三个技术点
**Q1:预约与点餐必须分开吗?系统能不能同时接预约订单和即时订单?**
如果只是功能上区分,可以在一张订单主表里加`order_type`字段(1即时点餐,2预约单)。但建议仍然保留独立的预约主表,原因是即时单不需要锁库存和锁时段,若混在一张表中,预约状态机的流转会干扰餐品订单的基础状态。
**Q2:核销通知是如何实时触达后厨或门店的?**
常见方案是WebSocket或SSE推送。用户核销成功后,服务端发送一个消息给后厨的接单大屏/后厨打印机(打印出餐小票);技术实现上可以在核销接口中同步订阅事件,基于Spring Boot的`ApplicationEventPublisher`发布事件,后厨监听端订阅待处理事件。
**Q3:核销时系统掉线了怎么办?如何保证不重复核销、也不漏核销?**
系统在离线时可用"预先下发批次码"方案处理。门店从管理后台下载一批备用码到本地设备,离线核销后在本地标记,等网络恢复后批量上传至服务器,服务端使用上文提到的UPDATE原子校验去重。若业务量不大,也可以直接依赖MySQL的索引兜底,确保一个码不可能在库中同时出现两次成功核销记录。
总体而言,"点餐预约核销系统"的实现难点并不在单一模块,而是把点餐业务流、预约时段库存、核销一次性校验串联在一个闭环中。合理设计状态机、控制并发扣减与核销原子性,配合Spring Boot + Uniapp的成熟技术栈,可以把系统的稳定性和可维护性提升上来,为后续向停车预约、家政上门等场景做能力复用打好基础。