从0到1开发快餐店扫码点餐系统:架构设计与实践
前阵子帮朋友打理一家社区快餐店发现每天中午高峰期点餐收银乱成一锅粥顾客排队到门口收银员一边听口头点单一边按计算器后厨出餐全凭吼。朋友随口问了一句“能不能搞个小程序扫码点餐”于是就有了这个代号 m080 的快餐店点餐服务系统。整套系统从需求梳理到上线跑通用了不到三周覆盖顾客扫码点餐、后厨大屏展示、收银台核销、经营报表四个核心场景。这篇文章把整个设计与实现过程完整拆开讲包括技术选型背后的理由、数据库和状态机的设计、接口实现的难点、前端交互的坑以及上线前必须检查的细节。如果你正准备给中小餐饮店做一套类似的点餐系统或者想了解一个完整业务系统从 0 到 1 怎么落地这篇应该能帮你少走不少弯路。1. 项目概述与需求拆解1.1 快餐店点餐的核心痛点做系统前我在这家店蹲了两天把真实流程摸了一遍。店铺面积不大约 80 平米就一张收银台、一个出餐口、后厨三个灶台。高峰期集中在 11:30 到 13:00大约 120 到 150 单。原来的流程是顾客到收银台看灯箱菜单口头点单收银员在收银机上选品、收款、打小票顾客拿着小票等叫号。问题非常明确点餐环节是串行的所有顾客都堵在收银台后厨却常常有空闲。没有完整的菜品图顾客对“红烧牛腩饭”和“咖喱鸡排饭”只能靠名字猜容易点错。收银员需要记住所有菜品编码和价格新人培训成本高高峰期容易按错。订单数据不完整每天卖了多少、哪个菜卖得好、损耗多少基本靠月底手工翻小票。这些痛点决定了系统不能只做一个“替代收银的电子菜单”而是要解决分流、数字化记录、后厨协同三件事。m080 这个代号就是当时的版本迭代编号后续还会继续滚动发布所以整体设计上我刻意保留了可扩展性。1.2 功能需求与角色划分经过和店长、收银员、后厨师傅分别聊过最终把角色定为四类角色使用终端核心操作顾客手机 H5 / 微信扫码浏览菜单、加购、下单、支付、查看取餐号收银员收银台 Web 管理端手动下单、改价、折扣、核销订单、退款、打印小票后厨厨房大屏KDS查看新订单、标记制作完成、催菜提醒店长/老板数据看板查看营业额、菜品销量、时段分析、退款记录这里有个容易忽略的点很多点餐系统只做“顾客自助下单”但快餐店实际运营中一定有现金支付、老人不会用手机、顾客临时要求去冰少盐之类的场景。所以系统必须保留“收银员代下单”的能力并且扣减库存、统计报表的逻辑要和自助下单完全一致。我在设计需求时把“扫码自助下单”和“收银台代下单”两条流程统一为同一个订单模型只是来源字段不同避免后期维护两套逻辑。1.3 非功能需求与选型约束快餐店的业务特点决定了几个硬性指标高峰并发按最坏情况估算1 分钟内可能有 30 个顾客同时加购下单数据库写入和支付回调要做到能扛住每秒 50 TPS 以上。稳定性优先店里的网络环境一般Wi-Fi 偶尔抖动不能因为网络差就丢单。订单数据必须本地落库前端要有重试机制。易用性后厨师傅平均年龄偏大大屏界面字号要大、颜色要分明、操作要少于两步。成本敏感小店不会养专门的运维所以部署结构越简单越好最好一台云服务器搞定所有服务。基于这些约束我没有选择微服务也没有引入消息队列和容器编排而是采用“单体应用 模块化拆分”的方式配合 Redis 做缓存和分布式锁。这个决策在下单高峰时被证明是对的系统足够简单出问题排查快一台 2 核 4G 的云服务器就能稳定运行月成本控制在几十块。2. 系统架构与核心设计思路2.1 整体技术选型技术栈选型主要围绕团队熟悉度、生态成熟度、招聘难易度来决定。我最终选用 Spring Boot 2.7 MyBatis-Plus MySQL 8.0 Redis Vue 3 Vant 4。后端用 Spring Boot因为 Java 生态对事务、支付对接、定时任务的处理最成熟出现诡异问题的概率低。前端顾客端用 Vue 3 Vant 4Vant 是移动端组件库表单、弹出层、数量步进器都有现成组件开发速度快。管理后台用 Vue 3 Element Plus桌面上操作密度高表格表单组件丰富。数据库用 MySQL数据量不大单表百万以内完全不是瓶颈但要注意索引设计和事务隔离级别。Redis 用来缓存菜单列表、购物车临时数据、用户登录态以及生成全局唯一的取餐号避免数据库自增主键暴露订单量。整个系统部署在一台阿里云 2 核 4G 的 ECS 上使用 Docker Compose 编排 Nginx、Spring Boot 应用、MySQL、Redis 四个容器。Nginx 承担静态资源托管和 API 反向代理同时配置了 HTTP/2 和 Gzip减少移动端加载时间。2.2 前后端交互流程设计顾客扫码后打开的是 H5 点餐页整个链路设计如下顾客扫描桌台二维码URL 携带 shopId 和 tableId例如https://order.example.com/h5?shopId1tableId8。前端加载时先请求后端/api/menu/list?categoryIdall获取菜单同时请求/api/cart/get获取该桌位 Redis 中缓存的购物车。顾客加购、修改数量时前端更新本地 Store并异步调用/api/cart/update把整个购物车快照同步到 Redis这样换手机扫码也能恢复购物车。提交订单时前端把购物车明细、备注、就餐人数、桌位号一并提交给后端/api/order/submit后端开启事务校验库存、生成订单、计算金额、清空购物车。后端返回订单号和预支付参数前端拉起微信支付或者支付宝支付。支付成功后后端收到回调更新订单状态为“已支付”并推送给后厨大屏。后厨大屏通过 WebSocket 监听新订单事件显示待制作条目。这里为了让点餐链路不至于被支付卡住我采用“先下单后支付”的模式用户点击“去结算”成功后订单已生成状态为“待支付”可以保留 15 分钟。超过 15 分钟未支付自动取消释放菜品库存。这样即使用户支付过程中断也不会产生超卖问题。2.3 关键业务模块划分从后端代码结构上划分为五个模块controller暴露 REST API包括菜单、购物车、订单、支付、管理端等。service业务逻辑包括订单状态流转、支付回调处理、库存增减、报表聚合。mapper数据访问层使用 MyBatis-Plus 的 BaseMapper 以及自定义 SQL。mq这里没有用专门的消息队列而是借助 Redis 的 Pub/Sub 和 WebSocket 实现事件通知后续如果量上来再替换成 RocketMQ。task定时任务包括取消超时订单、每日凌晨汇总报表、清理过期购物车缓存。前端顾客端、管理后台、厨房大屏是三个独立工程共享后端的 API 文档使用 Knife4j 生成 Swagger。厨房大屏不依赖复杂的 UI 框架直接用 Vue 3 CSS Grid 做成 1920x1080 的展示页面配置在电视机顶盒上自动打开浏览器全屏运行。3. 数据库设计与核心表单3.1 实体关系梳理餐饮系统的实体不算多核心围绕着“菜单—订单—支付”这几个概念展开。我一开始画了五张主表shop门店、category菜品分类、dish菜品、sku规格比如大份/小份、加冰/去冰、orders订单、order_item订单明细。再加三张辅助表cart购物车缓存但可以只存 Redis、payment_log支付流水、stock_log库存流水。门店、分类、菜品之间的关系是一对多菜品和规格是一对多。快餐店场景下规格比较简化比如饮品的大杯中杯、米饭的大份小份不涉及像服装那样的多层 SKU。设计sku表是为了后续扩展套餐和加料表结构如下CREATE TABLE sku ( id bigint NOT NULL AUTO_INCREMENT, dish_id bigint NOT NULL COMMENT 菜品ID, name varchar(50) NOT NULL COMMENT 规格名称如大份/小份, price decimal(10,2) NOT NULL COMMENT 售价, stock int NOT NULL DEFAULT 0 COMMENT 当前库存, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, sort int NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_dish_id (dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品规格表;3.2 关键表结构设计订单表是整个系统的核心字段设计时我特别注意区分“订单业务状态”和“支付状态”。早期很多开发会把这两个状态混在一个字段里导致后续退款、售后逻辑擦屁股很痛苦。我拆成两个字段CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务单号如M080202501031200001, shop_id bigint NOT NULL, table_id bigint DEFAULT NULL, customer_name varchar(50) DEFAULT NULL, total_amount decimal(10,2) NOT NULL, discount_amount decimal(10,2) NOT NULL DEFAULT 0.00, pay_amount decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3制作完成 4待取餐 5已完成 6已取消 7已退款, pay_status tinyint NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付 2已退款 3部分退款, pay_type tinyint DEFAULT NULL COMMENT 1微信 2支付宝 3现金, pay_time datetime DEFAULT NULL, source tinyint NOT NULL DEFAULT 1 COMMENT 1扫码自助 2收银台代下单, remark varchar(200) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_shop_status (shop_id, status, create_time), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;订单明细表order_item记录每个菜品的快照字段包括菜品名称、规格、单价、数量、做法备注。这里一定要做快照不能只存dish_id因为菜品价格和名称未来会修改历史订单必须保留当时的信息。库存设计值得一提。快餐店的库存不是实时扣毛料而是按“可售数量”控制。比如每天准备 50 份卤鸡腿顾客下单就扣减一份后厨出餐后发现实际报废了 2 份再通过后台的盘点功能修正库存。因此我建了stock_log表每次扣减都记录原因下单、取消、手工调整方便实时对账。3.3 订单状态机设计订单状态是最容易出边界 bug 的地方。我画了严格的状态机并且在后端 Service 层用状态机校验而不是让人在 Service 里随意order.setStatus()。核心流转如下待支付创建订单后初始状态。用户可取消、可支付超过 15 分钟系统自动取消。已支付支付回调成功后进入同时需要通知后厨。制作中后厨在 KDS 点击“开始制作”后进入。制作完成后厨点击“出餐”后进入此时顾客端显示“请取餐”。已完成收银员核销取餐码后进入也可以由系统在出餐后 30 分钟自动完成。已取消用户主动取消、超时取消或后台取消。已退款支付成功后发生整单退款时进入。我用一个OrderStateMachine类集中管理任何状态变更前先做合法性校验。例如“已支付”不能直接变成“已取消”必须经过“退款”流程。这个设计在后来的运营中帮了大忙因为退款请求时有发生如果状态随意跳转财务对账会乱成一团。4. 后端核心模块实现4.1 扫码点餐与购物车实现逻辑扫码点餐的第一步是解析二维码参数。店铺的每个桌位码固定生成格式为?shopId1tableId3二维码贴在桌上。用户扫出来的是 H5 页面地址Nginx 直接返回静态资源页面再根据 URL 参数请求后端接口。这里有个细节二维码要使用短链接或者固定地址不要包含特殊字符否则部分微信版本打开会截断参数。购物车设计我采用了 Redis 缓存键名是cart:{shopId}:{tableId}值为用户最近一次提交的购物车 JSON 快照。为什么不用 MySQL因为购物车是临时数据用户可能加菜后没下单就走了表里的数据就变成垃圾。Redis 可以设置过期时间比如 24 小时自动清理。唯一要注意的是并发问题同一桌位多个人同时加菜后写的覆盖先写的。我采用“读取—合并—写回”的策略在更新接口中先用 Lua 脚本做原子操作避免两个请求同时读旧值。核心加购逻辑封装在CartServicepublic synchronized Cart addCartItem(AddCartRequest request) { String key buildCartKey(request.getShopId(), request.getTableId()); Cart cart getCartFromRedis(key); OptionalCartItem exist cart.getItems().stream() .filter(i - i.getSkuId().equals(request.getSkuId())) .findFirst(); if (exist.isPresent()) { exist.get().setQuantity(exist.get().getQuantity() request.getQuantity()); } else { CartItem item new CartItem(); item.setSkuId(request.getSkuId()); item.setDishId(request.getDishId()); item.setDishName(request.getDishName()); item.setSpecName(request.getSpecName()); item.setPrice(request.getPrice()); item.setQuantity(request.getQuantity()); cart.getItems().add(item); } redisTemplate.opsForValue().set(key, JSON.toJSONString(cart), 24, TimeUnit.HOURS); return cart; }方法上加了synchronized虽然多实例部署时不那么优雅但对单实例部署的现状来说最简单可靠。如果后续水平扩展再改成 Redis 分布式锁也不迟。4.2 订单提交与支付流程订单提交是整个系统最需要小心的环节涉及事务、库存、金额计算、幂等。我执行的顺序是前端传递购物车快照后端根据 skuId 重新从数据库查询最新价格不能相信前端传的价格。校验库存用UPDATE sku SET stock stock - #{quantity} WHERE id #{skuId} AND stock #{quantity}语句原子扣减如果影响行数为 0 说明库存不足抛出业务异常。生成订单号规则是M080 yyyyMMddHHmmss 4位自增序号自增序号由 RedisINCR生成保证同一秒内不重复。计算总金额、优惠金额、实付金额如果是现金支付则直接置为已支付在线支付则生成支付单返回给前端。在同一个事务中插入订单主表和明细表并且把扣减库存和记录stock_log放在同一事务里。伪代码片段如下Transactional(rollbackFor Exception.class) public OrderSubmitResult submit(OrderSubmitRequest req) { // 1. 校验并查库获取最新价格 ListSku skuList skuMapper.selectBatchIds(req.getItemSkuIds()); // 2. 原子扣库存 for (OrderItemRequest item : req.getItems()) { int rows skuMapper.deductStock(item.getSkuId(), item.getQuantity()); if (rows 0) { throw new BizException(菜品[ item.getDishName() ]库存不足); } } // 3. 生成订单号 String orderNo generateOrderNo(); // 4. 组装订单和明细 Orders order buildOrder(req, orderNo); orderMapper.insert(order); // 5. 插入明细 orderItemMapper.batchInsert(order.getId(), req.getItems()); // 6. 记录库存流水 stockLogMapper.batchInsert(buildStockLogs(req)); return new OrderSubmitResult(orderNo, order.getPayAmount()); }支付回调处理也要考虑幂等。微信或支付宝回调可能会发送多次我用了payment_log表来记录回调流水号每次 callback 先查询流水是否已存在存在则直接返回成功。更新订单状态时采用UPDATE orders SET status 1, pay_status 1, pay_time NOW() WHERE order_no ? AND status 0保证只有待支付状态才能被更新为已支付防止并发重复处理。4.3 厨房KDS展示与状态同步后厨大屏是容易被低估的模块但实际使用频率最高。我采用 WebSocket 实现后端到前端的实时推送。Spring Boot 中配置一个TextWebSocketHandler前端在页面加载时通过ws://host/ws/kitchen建立连接。后端在订单支付成功后调用SendMessage推送一条NEW_ORDER事件内容为订单号和菜品明细。大屏页面分三列展示待制作、制作中、已完成。每个菜品卡片比较大正常坐在三米外能看清。后厨师傅点击“开始制作”后前端发送请求到/api/kitchen/start后端更新状态并广播给所有连接的大屏端让其他屏也能同步刷新。为了应对断网前端每 30 秒轮询一次/api/kitchen/pending作为兜底发现 WebSocket 断开就自动重连。这里有一个需要注意的体验问题同一个订单有多个菜品不能要求后厨一次性全部做完。比如一个订单包含煲仔饭和饮料煲仔饭要 8 分钟饮料是现成的。所以 KDS 支持按菜品维度操作而不是按订单维度。我专门建了一张order_item_status字段存在order_item表中后厨可以单独把某个菜品标记为“已完成”只有全部菜品完成后订单才允许进入“待取餐”状态。4.4 报表统计实现店长最关心的几个数据是今日营业额、订单量、客单价、TOP10 菜品、分类占比、时段分布、退款金额。这些数据如果直接实时查大表高峰期会拖慢主库我采用“定时汇总 实时查询”结合的方式。每天凌晨 2 点定时任务会扫描前一天的订单表把聚合数据写入daily_report表。报表接口优先查daily_report当天的实时数据则用缓存存储每 5 分钟通过聚合 SQL 刷新一次到 Redis。这样大屏上的“今日实时营业额”最多延迟 5 分钟对于快餐店老板来说完全够用。一段核心统计 SQL 示例SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS order_count, IFNULL(SUM(pay_amount), 0) AS total_amount, IFNULL(SUM(CASE WHEN source 1 THEN 1 ELSE 0 END), 0) AS scan_order_count, ROUND(IFNULL(AVG(pay_amount), 0), 2) AS avg_price FROM orders WHERE pay_status 1 AND status IN (1, 2, 3, 4, 5) AND create_time BETWEEN #{startTime} AND #{endTime} GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY day DESC注意统计时剔除退款订单只算pay_status 1且status处于有效范围避免把已退款的数据计入营业额。5. 前端与交互落地5.1 顾客端H5页面设计顾客端 H5 的原则是“三秒内能找到想点的菜”。首页采用顶部分类横向滚动、下方菜品列表纵向滚动的经典布局每个菜品卡片显示图片、名称、价格和加号按钮。底部是固定的购物车栏显示已选数量和总价点击弹出购物车明细侧边栏。菜单数据从后端接口获取后前端做了本地缓存设置 10 分钟过期。这样用户从菜单页到购物车页往返时不会重复加载。但要注意缓存不能影响库存展示如果某个菜品库存为 0后端接口会返回soldOut标识前端立即把卡片置灰并显示“售罄”。购物车交互我踩过一个坑Vant 的Stepper组件在数量为 0 时仍然会显示减号按钮点击后数量变成 -1。后来我监听change事件当数量小于等于 0 时弹窗确认是否删除该商品。如果直接删除用户误触就很烦。最终做法是数量减到 0 时不仅删除该项还同步调后端更新接口避免本地和 Redis 缓存不一致。H5 路由用的vue-router采用 history 模式Nginx 配置了try_files回退到index.html。支付成功后跳转到“订单详情页”页面展示取餐号和一个大的进度提示待支付、支付成功、制作中、请取餐。5.2 管理后台设计管理后台面向收银员和店长左侧菜单包括工作台、点餐收银、订单管理、菜品管理、分类管理、桌台管理、优惠券、数据报表和系统设置。收银台页面设计成类似 POS 的布局左侧是分类和菜品点击加购右侧是当前订单列表和结算按钮。收银员可以手动选择支付方式现金/微信/支付宝现金支付时直接点击“收款”即可。如果顾客已经在手机上自助下单但还没支付收银台也能看到该桌订单可以选择“协助支付”或“取消订单”。订单管理页需要支持按订单号、订单状态、手机号、时间段筛选。列表默认显示最近 7 天的数据超过 7 天默认从orders_history归档表查。这个归档策略是上线第二周加的因为发现订单表增长比预想快按天分区的 MySQL 表查询也变慢后来做了按月归档。菜品管理中最重要的是图片上传我建议直接用阿里云 OSS 或腾讯云 COS 存储不要存在本地服务器。菜品图片会频繁访问如果放在应用服务器磁盘网络 IO 会成为瓶颈。我使用前端直传 OSS 的方式后端只生成一个上传凭证这样能减轻带宽压力。5.3 接口联调与权限控制前后端分离开发时接口文档一定要提前定好。我先用 Knife4j 生成 Swagger 文档前端拿到文档后 mock 数据开发进度可以并行。联调阶段最容易出问题的三个点时间格式、金额精度、空值处理。时间格式统一使用yyyy-MM-dd HH:mm:ss后端的 LocalDateTime 通过 Jackson 配置全局格式化。金额一律用BigDecimal以“元”为单位传递前端展示时保留两位小数后端计算时杜绝浮点运算。空值方面前端需要兼容字段为null的情况比如备注没有填写时接口返回null如果直接渲染会显示undefined。我在 axios 响应拦截器里做了统一清洗把空字符串转成--。权限控制采用 Sa-Token 框架比 Shiro 轻量和 Spring Boot 集成也简单。顾客端接口通过SaIgnore注解放行部分路径管理端接口通过拦截器校验 token并按角色做二级校验。收银员只能操作订单和点餐店长才可以看到报表和系统设置。密码存储使用 BCrypt 加密不允许明文。6. 部署与上线检查清单6.1 环境部署部署上我用了 Docker Compose 编排一个docker-compose.yml文件拉起全部服务。Dockerfile中后端使用多阶段构建基础镜像选alpine以减少体积。前端构建后的dist目录直接挂载到 Nginx 容器的静态目录中。Nginx 关键配置节选server { listen 443 ssl http2; server_name order.example.com; ssl_certificate /etc/nginx/ssl/example.pem; ssl_certificate_key /etc/nginx/ssl/example.key; gzip on; gzip_types text/plain text/css application/json application/javascript; location / { root /usr/share/nginx/html/h5; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend: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; } }上线时要注意 HTTPS 证书的自动续期我直接使用了 certbot 的 docker 镜像定期刷新避免证书过期导致页面打不开。服务器防火墙只开放 80、443、22 端口后端服务的 8080 端口不直接暴露MySQL 和 Redis 也不暴露公网减少安全风险。6.2 上线前测试要点功能测试大家都会做但有几个场景特别容易漏支付成功回调延迟用户已经在收银台用现金支付了但微信支付回调后到需要保证订单不会从“已完成”变成“已支付”状态。超时未支付自动取消与用户同时点击支付两个请求并发时必须保证只能有一个成功。我在取消订单的方法上加了一个SELECT ... FOR UPDATE行锁先查订单状态如果是待支付才执行取消否则放弃。菜品库存临界值下单时库存剩 1 份两个用户同时下单最终只会成功一个扣库存 SQL 的原子性要专门压测。后厨大屏断网重连模拟关闭网络 30 秒再打开观察 WebSocket 心跳重连后是否能把断线期间的订单补拉回来。这些场景需要写自动化测试脚本我用 JUnit 5 的并发测试工具模拟多线程请求配合 MySQL 的事务回滚确保不依赖脏数据。一旦发现并发问题优先排查锁范围和事务边界。6.3 常见问题与排查实录上线两周整理了一份问题清单列几个最有代表性的现象原因解决方式顾客提交订单后页面一直转圈后端接口响应超时可能是 Redis 连接池满检查 Redis 最大连接数调整maxTotal为 200并设置合理等待时间后厨大屏偶尔闪退电视盒子内存不足WebSocket 断开后重连创建新实例未清理前端监听onbeforeunload主动断开电视盒子定时重启优惠金额计算错误使用了浮点型直接相减统一改用BigDecimal并保留两位小数扫描桌码进入后长时间空白二维码参数被微信转义成amp;Nginx 层做一次参数解码或者在生成二维码时使用短链服务退款后库存没有恢复退款逻辑里只改了订单状态没有调库存回补方法在退款事务中统一调用restoreStock(orderId)其中退款回补库存这个 bug 最隐蔽因为测试时往往只退一个完整订单但实际运营中会出现“部分退款”。我后来在payment_log和stock_log之间建立了关联每次部分退款都会在stock_log里记录负数流水方便追踪。还有一个小技巧所有对外返回的异常不要直接抛出系统异常堆栈需要统一包装成{code: 500, message: 库存不足}这样的结构。后端可以建一个全局异常处理器把BizException的 message 透传给前端前端弹 toast 提示。这样既不泄露数据库信息也能让顾客第一时间知道失败原因。我个人在实际操作中最深的体会是餐饮系统的难点从来不是技术框架而是对业务细节的把控。点餐、支付、后厨协同、库存、报表每个环节都有大量边界情况如果前期没有深入门店观察流程匆匆忙忙写代码上线后一定会在高峰期暴露问题。这套 m080 系统上线后朋友店的排队时长从平均 12 分钟降到了 5 分钟以内后厨师傅说大屏比吼叫清楚多了老板也能每天看报表做采购决策。如果你也在做类似的系统建议先花两天泡在店里把每个角色的真实动作记录下来再动手写第一行代码。