ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

开源多商户SaaS扫码点餐系统:架构设计与部署实战

开源多商户SaaS扫码点餐系统:架构设计与部署实战 做扫码点餐的项目我见过太多版本有的只是一张静态菜单页有的挂了复杂的后台却照顾不好一个门店还有的干脆做成定制项目每个商家拉一个分支、改一套逻辑后期维护成本高到怀疑人生。所以当我看到“多商户SaaS版扫码点餐系统完全开源”这个项目的时候第一反应是终于有人把这件事做对了。它解决的问题不是“做个点单网页”而是让一套代码同时服务成百上千个餐饮商家每个商家有自己的菜品、桌台、订单、支付配置互不干扰还能独立计费同时因为开源你既可以直接部署上线也可以拿它当作二次开发底座。这篇文章我想从产品定位、多租户架构、核心点餐流程、部署实操和问题排查五个维度把它掰开揉碎讲清楚给想快速落地或准备重构相关业务的团队一个完整参考。1. 项目概览与定位多商户SaaS到底在解决什么问题1.1 不是“扫码下单”这么简单而是“一套代码养活万家店”很多刚接触这个项目的人会问扫码点餐不就是一个H5页面加一个订单表吗如果只做单店demo确实如此。但多商户SaaS的核心难点在于“多”字菜品数据怎么隔离、订单怎么归属、支付回调怎么区分商户、套餐过期怎么限制功能这些都需要在产品架构层面提前设计而不是事后打补丁。这个项目把角色分成了三类平台运营者负责商户审核、套餐发布、系统配置看得见所有数据但不直接参与单个门店的日常运营。商户餐饮店老板/店员管理自己的菜品分类、商品、桌台、打印机处理本店订单查看营业报表。顾客扫桌台码进入点餐页加购、下单、支付全程不需要装App也不需要关注公众号。三者通过一套系统串联起来平台端负责“管商户”商户端负责“管店”顾客端负责“下单”。正因为职责边界清晰系统才扛得住多商户并行使用而且每个商户的定制需求可以通过配置项而不是改代码来满足。1.2 “完全开源”的意义可私有化、可定制、数据可控餐饮行业对数据敏感度很高门店流水、菜品销量、顾客手机号、支付渠道对账单这些都是核心资产。直接用市面上闭源SaaS数据全部存在别人服务器上一旦平台调整政策或涨价自己非常被动。而开源的ERP系统至少有三层好处部署可控源码在自己的服务器或云主机上跑数据库在自己手里。改造可控需要对接本地团购平台、自有会员系统、特殊打印模板时直接改代码。成本可控没有按年收取的“系统使用费”只需要承担服务器、域名和支付接口成本。当然开源不等于零门槛你要有一定的Java后端和前端基础至少能处理部署、配置、日志排查。如果完全没接触过服务器和数据库建议先在本地虚拟机把项目跑通再考虑上线。2. 核心技术设计与多租户实现方案2.1 技术栈选型为什么我认可这套组合这套系统选用的是Spring Boot MyBatis Plus MySQL Vue前后端分离的经典架构。Spring Boot负责业务接口和事务控制MyBatis Plus简化数据库操作Vue负责后台管理页面和用户端H5。比起Spring Cloud全家桶它足够轻比起PHP系传统框架它的类型约束和工程化程度更高适合中大型团队维护和扩展。具体来说我比较关注的是这几个技术决策Redis承担会话和缓存用户登录态、短信验证码、高频访问的菜单数据都放Redis数据库压力小很多。RabbitMQ或者同类消息队列处理异步任务订单创建后的消息通知、打印任务投递、支付回调后的异步处理都通过队列解耦避免同步请求把核心下单链路拖慢。Nginx作为前端入口和反向代理静态页面和API请求分流后续加负载均衡也方便。这套组合的好处是招聘容易、组件成熟、踩坑资料多。就算你的团队之前没接触过这个项目只要熟悉Spring Boot基本看一遍代码结构就能上路。2.2 多租户数据隔离共享数据库 租户ID而不是一个商户一套库多租户最常见的两种方案一是每个商户独立数据库数据隔离最彻底但维护成本和资源占用高二是所有商户共享数据库通过租户ID字段区分数据。这个项目采用的是后者加一点变通核心业务表都带tenant_id商户ID字段并在查询层强制带上租户条件从代码层面保证商户A永远看不到商户B的数据。共享数据库加租户ID的方案在几百上千个商户的规模下完全够用而且备份、迁移、报表统计都极其方便。一旦某个租户数据量特别大还可以后续迁移到独立库。实现层面的核心手段是MyBatis Plus的租户插件或者自定义拦截器自动在SQL语句尾部拼接AND tenant_id ?应用层开发时甚至不用手动关心这个条件。这种做法有一个关键点明细表也要带tenant_id不能只靠主表。比如订单主表有租户ID订单明细表没有一旦后厨打印或者退款需要按明细查询就容易串数据。所以建表的时候就把所有表都加上租户ID列一劳永逸。2.3 租户上下文的传递从请求进入网关的那一秒开始绑定租户ID不能靠前端传一个参数就信正规的做法是在登录认证后从Token或者登录态缓存里解析出用户所属商户ID写入一个线程级的租户上下文。后续这个线程处理的MyBatis查询、业务逻辑、消息发送都从上下文拿租户ID。请求结束必须清空上下文否则线程池复用会导致租户ID串号。给大家看一段最简化的拦截器示意public class TenantContext { private static final ThreadLocalString CURRENT_TENANT new ThreadLocal(); public static void setTenantId(String tenantId) { CURRENT_TENANT.set(tenantId); } public static String getTenantId() { return CURRENT_TENANT.get(); } public static void clear() { CURRENT_TENANT.remove(); } }public class TenantInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); LoginUser user JwtUtils.parseToken(token); TenantContext.setTenantId(user.getTenantId()); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { TenantContext.clear(); } }这段代码很简单但代表了整个系统的数据隔离基石。复杂一点的做法是再配合MyBatis拦截器统一拼SQL条件不用每个Mapper手动加条件。压测下来这种方案的性能损耗很小多租户场景收益却很大。2.4 商户级独立配置支付、打印、短信都按租户走扫码点餐涉及到的外部服务比如微信支付、支付宝、云打印机、短信服务这些不是平台统一一个账号就能搞定的。每个商户可能用自己的营业执照和支付商户号打印机的品牌型号也各不相同。所以系统里必须有一张“商户配置表”存每个租户的支付参数、打印参数、短信通道参数。特别提醒支付回调地址如果是统一的入口那么回调逻辑首先要根据回调参数里的商户号比如out_trade_no前缀或者attach字段解析出对应的租户ID再去加载对方的密钥和解密逻辑绝对不能拿平台全局的支付配置去验所有商户的签名。3. 扫码点餐核心流程的实现细节3.1 桌台码与菜单绑定一个二维码背后的关联关系顾客扫码看到的是“这个门店、这个桌台”对应的菜单这背后涉及四层数据关联桌台码二维码内容→ 门店ID → 桌台ID → 菜单分类与商品。二维码内容一般是个短链接或者参数串例如https://your.domain/s/{shopId}?table{tableId}。顾客打开后前端通过门店ID和桌台ID调用菜单接口。菜单接口要做两层缓存一是门店维度的菜品缓存减少数据库查询二是商品状态缓存如果有商品下架或者库存不足要实时生效。实际业务里容易出现一个问题顾客扫码进页面一直挂着不操作店员在后台把某个商品下架了顾客这时还能不能加购我建议至少做到准实时点餐页每隔30秒拉一次菜单版本的变更标记有变动就提示刷新。这个版本号可以放到Redis里每次商品变更就自增。3.2 购物车、下单与支付回调的一致性保障顾客把菜加入购物车点击结算后系统创建一条待支付订单。这个阶段要特别注意两点一是订单金额必须以后端重新计算为准前端传过来的总价只是展示用不能直接信任二是下单时锁定库存或者按当日备货量校验防止超卖。支付成功后的回调处理是整个系统的核心咽喉推荐做法是接收微信/支付宝回调后先验签再查单再更新本地订单状态。用分布式锁或者数据库唯一约束保证同一笔订单的重复回调只处理一次。订单状态更新和“加积分/发消息/通知后厨”这些下游操作放到消息队列里异步执行。用户端通过轮询或者WebSocket感知支付结果而不是依赖回调的即时性。我还见过很多团队在回调业务里直接写一堆短信发送、打印逻辑一旦某个环节异常整个事务回滚导致订单已支付但系统显示未支付。正确的做法是把“改状态”和“后续动作”分开状态变更做完立即提交事务后续动作失败可以通过定时任务补偿。3.3 后厨打印与出餐状态流转下单支付成功后订单要流转到后厨。这个项目里打印任务不是直接从前端调打印机而是后端生成打印数据投递到消息队列再由打印服务或者对接云打印平台推送到对应门店的打印机。这样做的好处是前端不管打印机在线与否只要订单成功任务就进队列了打印机不在线时任务保留设备恢复后自动补打。出餐状态流转我建议做成这样已支付→已接单→制作中→已出餐→已上桌。店员在商户后台或者后厨平板上点“接单”系统记录接单人和时间方便后续扯皮时溯源。如果门店有“并台”或“拆单”需求也就是顾客加菜、换桌、多人分账那订单结构要做好子订单设计。我的经验是主订单放桌台和顾客信息子订单放菜品明细结账时把子订单合并或者分别结算灵活度更高。4. 部署与二次开发实操记录4.1 本地快速启动从源码到跑起来先把项目克隆下来准备环境JDK 8以上、Maven 3.6、MySQL 5.7、Redis 5.0。数据库初始化一般有SQL脚本直接执行后会创建平台库和基础数据表。开发环境的application.yml里改四个配置就可以启动数据库连接地址、账号、密码Redis连接地址、密码JWT签名的密钥必须要改不能用默认值文件上传的本地路径或OSS配置启动顺序一般先是后端服务再是后台管理前端最后是用户端H5。启动后端时注意看启动日志里有没有报“表不存在”或者“租户配置缺失”的错误如果是租户插件生效导致的查询异常先检查数据库是否执行了全部的表结构脚本。有个坑要提前说开源项目默认的Redis数据库索引可能是0如果你的Redis上还跑着其他服务建议在配置里改成单独的db编号比如database: 5避免key冲突导致数据互相干扰。4.2 生产部署服务器、域名与HTTPS生产环境我建议直接用Docker Compose编排把MySQL、Redis、后端服务、前端静态页面、Nginx放在一个网络里。单个门店场景2核4G的云服务器就够用了商户量大的情况把MySQL和Redis放到独立机器。配置HTTPS是必须的尤其是涉及微信支付回调的场景微信要求回调地址必须是HTTPS。Nginx里需要把/api/路径代理到后端服务端口同时处理前端静态文件。4.3 二次开发的工程结构要点我看了这个项目的代码结构核心要点体现在下面几个模块上平台管理端商户审核、套餐管理、全局配置。商户后台商品管理、桌台管理、订单管理、打印机管理、营业报表。用户端API菜单浏览、购物车、下单支付、订单查询。消息与任务模块短信、邮件、队列任务、定时任务。如果你要扩展功能比如增加“会员储值”或“门店自提”优先在商户后台模块加接口用户端API模块加对应前端请求平台端只需要处理新增的计费项和权限点。数据库层面新增表记得带tenant_id并且检查MyBatis拦截器是否会自动加租户条件。5. 常见问题与排查技巧实录5.1 支付回调丢失或延迟订单一直显示“待支付”这是扫码点餐业务里最频繁的线上故障。排查思路分三步先看网关日志里有没有收到支付平台的回调请求再看回调处理服务有没有报验签失败最后看订单状态更新事务有没有被回滚。如果确认回调根本没进系统多半是Nginx里没有把支付回调路径代理到后端或者HTTPS证书配置不完整。我建议加一个定时任务每5分钟扫描一次超过10分钟仍未支付的订单主动调用支付平台查单接口以查单结果为准修正本地状态。这个兜底方案虽然不是最优解但能极大降低漏单率。5.2 打印机重复打印或者漏单云打印机最常见的坑是“一次订单打两遍”。排查后发现是打印回调接口处理了两次消息或者消费队列时没有做消息幂等。给每个打印任务生成一个全局唯一taskId打印服务记录已处理过的taskId重复消息直接丢弃。对于漏单场景建议在“订单详情页”提供“手动补打”按钮总比后厨跑过来说没单强。另一个问题后厨打印出来菜品名称乱码。基本都是打印模板编码问题云打印机一般要求GBK编码后端推送前做一次编码转换就能解决。5.3 高峰期同时下单菜品超卖秒杀式的“热门菜加购”在餐饮场景也会出现尤其是开业活动或者外卖渠道引流的时候。加购和下单分开处理先通过Redis预减库存库存不足直接提示“已售罄”成功占位的再进入创建订单流程。如果库存扣减成功但订单创建失败要有一个定时任务做库存回滚否则会把库存白白扣掉。提到超卖顺手说一个数据一致性的问题同一条订单记录要防止被并发操作覆盖。更新订单状态时建议加上状态条件比如UPDATE orders SET status 1 WHERE id ? AND status 0。这样就算两个请求同时到达也只有一条能更新成功。5.4 问题排查速查表现象大概率原因排查方法解决方案回调收到但订单未支付验签失败或状态更新逻辑异常查看回调日志与订单日志重新配置商户证书密钥检查回调处理是否幂等部分商户菜单加载慢Redis缓存未命中或热点门店压力大查看缓存命中率和SQL日志增加二级缓存设置合理的缓存过期时间打印漏单队列消费者异常或设备离线查看队列堆积情况和打印日志补打功能兜底消息消费失败次数达到阈值后告警跨商户数据串了租户ID未正确写入上下文检查登录鉴权后的上下文赋值框架层强制SQL追加租户条件前端页面白屏后端接口报500查看后端日志和前端console报错检查数据库连接池配置及接口参数我最后再分享一个实践中的判断不要把“多商户SaaS扫码点餐”看作一个固定功能清单它其实是一套可生长的基础设施。跑通基础点餐后你会发现加一个“预约排队”、加一个“会员储值”、加一个“连锁门店管理”都是顺着多租户架构平滑加上去的而不是推翻重来。这也是开源项目最大的魅力——你可以把别人的积累拿过来站在一个已经被验证过的地基上继续盖楼。如果你手头正好需要一套点餐系统或者在考虑自研建议先把这个项目完整跑一遍很多纠结的问题自然就有答案了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进