无线点餐系统源码实战:SpringBoot与WebSocket订单幂等及后厨推送

发布时间:2026/10/30 6:56:09
无线点餐系统源码实战:SpringBoot与WebSocket订单幂等及后厨推送 简介面向高校计算机专业毕业设计场景的 Android 无线点餐系统完整项目包围绕客户端点菜与后台服务端的业务流程展开适合正在准备毕业设计、课程设计或需要一套可运行订餐系统原型的同学参考。压缩包共 365 个文件、约 9.99MB。其中75 个 java 源文件与 109 个 class 文件构成登录、桌台管理、菜单浏览、下单与结算等核心逻辑30 个 xml 承载界面布局与工程配置61 张 jpg 与 18 个 gif 提供菜品图和界面素材18 个 jar 为依赖库11 个 jsp 对应后台页面另有 6 份 doc 说明文档和 apk 安装包便于直接部署演示。目前已有 239 人学习。读者可从中获得一套结构完整的点餐系统代码骨架、数据库与页面资源以及从点菜到支付的流程实现参考可用作论文撰写与答辩演示的基础材料。1. 从选题表上的那行字说起无线点餐系统真正难的部分不在点菜页不少人第一次接触无线点餐系统是在毕业设计选题表上看到的那行描述顾客扫码、平板点单、后厨出单、前台结账。真动手做才发现最不值钱的就是那个点菜页面两天能画完剩下的时间全耗在桌台状态、订单幂等和后厨长连接上。这套源码就是围绕这条链路写的顾客端 H5 扫码开台点菜后厨大屏实时收单前台管桌台和结账服务端一套 SpringBoot 兜住所有状态。它适合三类人拿毕业设计当练手、需要一份能跑通也能讲清架构的底稿想给自家小店搭一套、能改菜单就能用的开发者以及想看清「订单状态机 实时推送」怎么配合的初中级后端。2. 架构与技术选型从顾客 Pad 到后厨大屏的一条数据链2.1 四端一服谁在跟谁说话这个项目把系统拆成四个客户端加一个服务端划分依据是「谁坐在哪个位置、盯的是哪块屏」而不是技术栈。顾客端是 H5 页面跑在手机或平板浏览器里不装 App。这一点很关键也是「无线点餐」四个字真正的落地方式桌角贴二维码顾客扫码直接进点餐页平板由店家统一采买、固定在支架上浏览器打开就是点餐界面。免安装带来的好处不只是省事版本更新只需重新部署一次静态资源所有终端下次刷新即生效答辩现场换台设备也不用重新装包。后厨端是一块挂在出餐口的显示器只做三件事新单响铃、按菜品分类聚合展示、点「已出餐」。它不参与任何业务写操作只消费推送、回写状态职责越窄越不容易出岔子。前台管理端负责桌台管理开台、并台、清台、菜单维护分类、菜品、规格、估清、订单查询和结账。服务端则是单体 SpringBoot 应用对外暴露 REST 接口给三个 Web 端同时开一条 WebSocket 通道专门推后厨。一条完整数据链是这样的顾客扫码 → 服务端校验桌台并下发会话 sessionId → 顾客加购物车暂存 Redis→ 提交订单落库 事务→ 事务提交后推 WebSocket → 后厨接单改状态 → 出餐 → 前台结账 → 清台释放会话。这条链上任何一环的状态没对齐后面就会出乱子第 5 章会专门讲这些。2.2 技术栈清单与选型理由层次选型为什么是它服务端框架SpringBoot 2.7起步快答辩时能讲清分层生态里 MyBatis-Plus、Redis 客户端都是现成的持久层MyBatis-Plus MySQL 8单表 CRUD 几乎不用写 SQL复杂一点的报表再用 XML 手写缓存Redis购物车、幂等键、桌台会话、后厨在线连接索引都放这里实时推送WebSocketSpring 原生 Handler后厨要的是服务端主动推轮询在高峰期会拖垮接口管理端Vue3 Element Plus Vite表格、表单组件全写后台省一半时间顾客端Vue3 Vant移动端组件贴合手指操作体积小弱网也能开鉴权JWT 拦截器前后端分离无状态部署不用管 session 共享部署Nginx 单机 jar静态资源和接口同域省掉跨域配置这一堆麻烦选单体不选微服务理由很实在这套系统的并发量就是一家店几十张桌拆成订单服务、菜品服务、推送服务除了让部署变复杂没有任何收益。把模块在代码里分清楚就够了包结构上按业务切比按 controller/service/mapper 切更好维护下一章会展开。2.3 数据模型五张表撑起整条点餐流程核心表只有五张桌台表t_table、菜品表t_dish、订单主表t_order、订单明细表t_order_item、员工表t_user。菜品分类t_category和规格t_dish_spec属于锦上添花先不展开。设计上有一个绕不开的决定订单明细要不要冗余菜名和单价。答案是必须冗余。菜品改价、下架、改名在真实场景里天天发生如果明细只存dish_id历史订单打印出来就是空白或者错价对账时根本说不清。这是很多毕设模板会漏掉的一点。CREATE TABLE t_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 对外单号展示和打印用, table_id BIGINT NOT NULL COMMENT 桌台 id, session_id VARCHAR(64) NOT NULL COMMENT 开台会话一桌一次翻台即失效, client_token VARCHAR(64) NOT NULL COMMENT 幂等键由前端进入点餐页时生成, total_amount DECIMAL(10,2) NOT NULL COMMENT 主单金额等于明细求和, status TINYINT NOT NULL DEFAULT 10 COMMENT 10待接单 20制作中 30已出餐 40已结账 90已取消, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_client_token (client_token), -- 数据库层兜底幂等 KEY idx_table_session (table_id, session_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_order_item ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, dish_name VARCHAR(64) NOT NULL COMMENT 下单时快照菜品改名不影响历史单, spec_text VARCHAR(64) DEFAULT NULL COMMENT 如「大份/微辣」同样是快照, price DECIMAL(10,2) NOT NULL COMMENT 成交单价含规格加价, qty INT NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_client_token这个唯一索引是最后一道防线。哪怕 Redis 挂了、代码里的幂等判断被绕过数据库也会把重复提交拦下来抛DuplicateKeyException业务层捕获后转成「订单已提交请勿重复操作」返回给前端。idx_table_session则是给「查这一桌当前有哪些单」这个高频查询准备的加菜、并单、结账都要走它。3. 目录结构与本地启动把项目从压缩包跑到浏览器里3.1 后端目录按业务切不按技术切wd-order/ ├── server/ # SpringBoot 单体 │ ├── src/main/java/com/example/order/ │ │ ├── OrderApplication.java │ │ ├── common/ # 统一返回、异常、拦截器、JWT 工具 │ │ ├── table/ # 桌台开台、并台、清台 │ │ ├── dish/ # 菜品分类、规格、估清 │ │ ├── cart/ # 购物车只碰 Redis │ │ ├── order/ # 下单、加菜、退菜、结账 │ │ ├── kds/ # 后厨推送与连接管理 │ │ └── printer/ # 小票机 ESC/POS 指令封装 │ └── src/main/resources/ │ ├── application.yml │ └── mapper/ # 少量复杂 SQL 的 XML ├── admin-web/ # 前台管理端 Vue3 ├── guest-h5/ # 顾客点餐端 Vue3 Vant ├── kds-web/ # 后厨大屏 Vue3 └── sql/wd_order.sql # 建库建表 演示数据table、dish、order这些包内部各自带 controller、service、mapper、entity一个业务一个纵切。好处是改「退菜」逻辑时只需要打开order一个目录不用在三个技术层之间来回跳。这种切法在毕业设计里还有个加分项答辩时老师问「你这个模块怎么分的」你可以直接指着目录说业务边界。kds包是独立的因为它不属于任何一个业务域它只订阅订单事件。order服务里通过事件或者直接调用通知器把新单丢过去两边耦合度压到最低。printer单独拎出来也是同理小票打印失败不能影响下单主流程。3.2 三个前端工程的目录与路由guest-h5是顾客端路由只有四个扫码落地页带桌台参数、菜单页、购物车抽屉、订单详情。它没有登录身份靠 URL 上的桌台号和 sessionId 识别这是刻意的让顾客输账号密码等于劝退。kds-web只有一个页面路由都省了全屏铺开按菜品分类分列展示待制作项。它的技术重点不在 UI在重连逻辑4.3 会讲。admin-web路由按后台菜单走/table、/dish、/order、/stats。路由守卫里统一做 token 校验401 直接踢回登录页。三个前端共用一份接口类型定义常见做法是新建一个shared/types目录用相对路径引用或者打成私有包。这个项目体量小用的是相对路径省掉发包的麻烦。3.3 从建库到登录可抄的启动步骤先改配置再导数据最后按顺序起服务。# server/src/main/resources/application.yml server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/wd_order?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: 127.0.0.1 port: 6379 database: 0 mybatis-plus: configuration: map-underscore-to-camel-case: true # 下划线转驼峰实体类不用逐个写 TableField # 自定义配置JWT 和打印 app: jwt: secret: change-me-in-production # 上线必须换否则谁都能伪造 token expire-hours: 12 printer: enabled: false # 没接小票机就先关掉不然启动会报端口占用app.printer.enabled这个开关是血泪经验。项目里打印走的是 USB 虚拟串口没插机器的情况下打开会一直重连日志刷屏把真正的报错淹掉。第一次跑先关掉等页面能点通了再开。# 1. 建库并导入表结构和演示菜品 mysql -uroot -p123456 -e CREATE DATABASE wd_order DEFAULT CHARSET utf8mb4; mysql -uroot -p123456 wd_order sql/wd_order.sql # 2. 起后端8080 端口 cd server mvn clean package -DskipTests java -jar target/wd-order-1.0.0.jar # 3. 管理端默认 5173账号 admin / 123456 cd admin-web npm install npm run dev # 4. 顾客端--host 让同一局域网内的手机平板能访问 cd guest-h5 npm install npm run dev -- --host 0.0.0.0第四步的--host 0.0.0.0不能省。默认 Vite 只监听127.0.0.1平板输电脑的内网 IP 是打不开的控制台还会给一句很容易被忽略的提示。启动后看终端打印的 Network 地址那串http://192.168.x.x:5174才是平板要访问的地址把它生成二维码贴在桌角就是一套能演示的无线点餐。验证顺序建议是管理端能登录 → 菜品列表有数据 → 顾客端能开到台 → 提交一单 → 后厨大屏弹出来。四步全通说明整条链路的配置没问题再开始改代码。4. 核心链路拆解一次下单请求从提交到后厨出单4.1 桌台会话先有「这一桌」才有购物车购物车不能只按顾客身份存因为顾客没有身份。这个项目用的是「桌台会话」模型顾客扫码时服务端根据tableId查当前是否有活跃会话没有就开台并生成一个sessionId有就直接复用。同一个sessionId下所有终端共享同一个购物车两三个人同时扫码点菜不会各点各的。Service public class CartService { Autowired private RedisTemplateString, Object redis; /** * 加菜同一个菜品 同一个规格算作一行数量累加 * param sessionId 开台时下发的会话 id一桌一个 * param dishId 菜品 id * param specKey 规格组合的稳定标识如 sizeL;sugar0 */ public void addItem(String sessionId, Long dishId, String specKey, int qty) { if (qty 0) { throw new BizException(数量必须大于 0); } String key cart: sessionId; String field dishId : specKey; // hash field 天然去重 redis.opsForHash().increment(key, field, qty); redis.expire(key, Duration.ofHours(4)); // 一桌翻台周期内有效过期自动清 } /** 减菜减到 0 直接删 field别在 hash 里留一堆 0 */ public void removeItem(String sessionId, Long dishId, String specKey, int qty) { String key cart: sessionId; String field dishId : specKey; Long left redis.opsForHash().increment(key, field, -qty); if (left ! null left 0) { redis.opsForHash().delete(key, field); } } }用 Hash 而不是存一个 JSON 字符串是为了让「加一份」这个动作变成HINCRBY的原子操作。如果存 JSON两个人同时加菜就是典型的读改写竞态后写的人会把先写的人覆盖掉高峰期这一桌的菜就凭空少了一份。expire每次写入都刷新保证一桌饭吃三小时购物车也不会莫名消失反过来如果顾客扫码后一直没下单就走了四小时后 Key 自动过期不会在 Redis 里堆垃圾。specKey的拼法要固定顺序sizeL;sugar0和sugar0;sizeL必须归一到同一个字符串否则同一份「大杯无糖」会被当成两个购物车行后厨看着也懵。4.2 下单接口事务、幂等和金额校验下单是整个系统里唯一必须开事务的地方也是最容易翻车的地方。前端的提交按钮点快了、弱网重试、用户手抖双击都会造成重复下单后厨就会收到两份。Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private OrderItemMapper itemMapper; Autowired private DishMapper dishMapper; Autowired private TableMapper tableMapper; Autowired private RedisTemplateString, Object redis; Autowired private KdsNotifier notifier; Autowired private IdGenerator idGen; Override Transactional(rollbackFor Exception.class) public OrderVO submit(OrderSubmitReq req) { // 1. 幂等clientToken 由前端进点餐页时生成一次重试复用同一个 String idemKey order:idem: req.getClientToken(); Boolean first redis.opsForValue() .setIfAbsent(idemKey, req.getSessionId(), Duration.ofMinutes(10)); if (Boolean.FALSE.equals(first)) { // 已经下过了直接把上一次的单原样返回前端体验上等同于成功 return orderMapper.selectVOByToken(req.getClientToken()); } // 2. 桌台校验脏桌台未开台或已清台不允许下单 TableInfo table tableMapper.selectById(req.getTableId()); if (table null || table.getStatus() TableStatus.FREE.getCode()) { throw new BizException(桌台已清台请重新扫码); } // 3. 金额以后端为准前端传的 price 一律不采信 BigDecimal total BigDecimal.ZERO; ListOrderItem items new ArrayList(); for (OrderItemReq it : req.getItems()) { Dish dish dishMapper.selectById(it.getDishId()); if (dish null || dish.getStatus() DishStatus.OFF.getCode()) { throw new BizException(菜品已估清 it.getDishId()); } BigDecimal extra it.getSpecExtra() null ? BigDecimal.ZERO : it.getSpecExtra(); BigDecimal price dish.getPrice().add(extra); // 规格加价 total total.add(price.multiply(BigDecimal.valueOf(it.getQty()))); items.add(buildItemSnapshot(dish, it, price)); // 冗余菜名和规格文本 } // 4. 落主单 明细 Order order new Order(); order.setOrderNo(idGen.next()); order.setTableId(req.getTableId()); order.setSessionId(req.getSessionId()); order.setClientToken(req.getClientToken()); order.setTotalAmount(total); order.setStatus(OrderStatus.WAIT_ACCEPT.getCode()); orderMapper.insert(order); for (OrderItem i : items) { i.setOrderId(order.getId()); itemMapper.insert(i); } // 5. 事务提交后再推后厨避免后厨先看到单、库里却回滚了 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { Override public void afterCommit() { notifier.pushNewOrder(order, items); redis.delete(cart: req.getSessionId()); // 下单成功清购物车 } }); return toVO(order, items); } }几个细节值得说清楚。幂等键放在 Redis过期时间 10 分钟比任何一次网络重试都长。如果 Redis 不可用还有数据库唯一索引兜底insert抛DuplicateKeyException时业务层捕获并转成友好提示。金额一定用BigDecimaldouble在0.1 0.2上就会给你惊喜对账差三毛钱的问题能查到怀疑人生。规格加价也走BigDecimal从菜品配置里读不接受前端传的任何价格。最容易被忽略的是第 5 步的afterCommit。如果直接在事务里推 WebSocket推送是先于提交执行的后厨大屏「叮」一声弹出新单一秒钟后数据库回滚了后厨做了一份不存在的菜。这个 bug 在本地测试时几乎发现不了因为本地不会失败只有线上出异常才暴露属于典型的黑匣子问题。后厨那边的推送失败要不要重试这个项目选择了「不重试落库即算数」。KDS 每次刷新页面都会拉一次「待接单列表」推送只是加速手段不是唯一数据源。这个取舍很重要把推送当成优化而不是数据通道系统就不会因为一条连接断了而丢单。4.3 后厨推送连接管理与断线重连后厨端是整个系统里唯一要求实时的地方用 WebSocket 而不是轮询。轮询的代价在高峰期很明显十块屏每两秒拉一次接口一天下来十几万次请求全打在同一张订单表上。Component public class KdsSessionManager { // storeId - 该门店所有在线的后厨终端 private final MapString, SetKdsSession sessions new ConcurrentHashMap(); public void add(String storeId, WebSocketSession raw) { sessions.computeIfAbsent(storeId, k - ConcurrentHashMap.newKeySet()) .add(new KdsSession(raw)); } /** 广播新单顺手清掉已经死掉的连接别让它们拖慢整轮推送 */ public void broadcast(String storeId, Object payload) { SetKdsSession set sessions.getOrDefault(storeId, Collections.emptySet()); String json JsonUtil.toJson(payload); for (KdsSession s : set) { try { if (s.getRaw().isOpen()) { s.send(json); // 内部加锁WebSocketSession 非线程安全 } else { set.remove(s); } } catch (Exception e) { set.remove(s); // 发送失败说明连接已废直接摘掉 } } } /** 心跳客户端每 30 秒发一次服务端超过 90 秒没收到就判定掉线 */ public void onPong(String storeId, String clientId) { // 更新最后活跃时间由定时任务清理超时连接 } }WebSocketSession的sendMessage不是线程安全的多个线程同时往同一个连接写会抛IllegalStateException所以KdsSession内部包了一层synchronized。这个坑不踩一次很难想到因为单线程测试永远正常。前端这一侧的重连逻辑比服务端更容易写错。裸写setInterval重连的结果是网络恢复的瞬间几十个重连请求同时打过来。正确做法是指数退避并且连上之后重置计数。// kds-web/src/utils/socket.js let ws null; let retry 0; let heartbeat null; export function connect(storeId, token, onMessage) { const proto location.protocol https: ? wss : ws; ws new WebSocket(${proto}://${location.host}/ws/kds/${storeId}?token${token}); ws.onopen () { retry 0; // 连上就归零下次断线仍然从 1s 开始 heartbeat setInterval(() { if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping })); } }, 30000); // 30s 心跳压住 Nginx 的 60s 空闲超时 }; ws.onmessage (e) { const msg JSON.parse(e.data); if (msg.type pong) return; // 心跳回包不往业务里丢 onMessage(msg); // NEW_ORDER / CANCEL_ORDER / URGE }; ws.onclose () { clearInterval(heartbeat); const delay Math.min(1000 * Math.pow(2, retry), 30000); // 1s 2s 4s…封顶 30s setTimeout(() connect(storeId, token, onMessage), delay); }; ws.onerror () ws.close(); // 交给 onclose 统一处理别写两套重连 }心跳的间隔要小于反向代理的空闲超时。Nginx 默认proxy_read_timeout是 60 秒如果心跳是 90 秒一次连接会被中间层悄悄掐掉而浏览器这一侧要过一会儿才知道表现就是「大屏不响了刷新一下又好了」。这个问题在第 5 章还会展开。4.4 加菜、退菜与并单订单状态机的边界订单状态只有五个待接单、制作中、已出餐、已结账、已取消。看起来简单但真实场景里的动作比想象中多。加菜不是新建一张独立订单而是往同一个sessionId下追加一张子单主单金额重算。这样后厨看到的是「3 号桌第二批次」出餐时不会把两次的菜混在一起。实现上给t_order加一个batch_no字段同一个 session 内自增。退菜必须限制状态。已经「已出餐」的单不允许退只能走「赠送/折扣」流程否则后厨的菜已经做出来了账也对不上。代码里做的是一张状态流转表用Map当前状态, 允许的下一状态集合表达比一堆if-else好维护得多public enum OrderStatus { WAIT_ACCEPT(10), COOKING(20), SERVED(30), PAID(40), CANCELED(90); private static final MapOrderStatus, SetOrderStatus FLOW Map.of( WAIT_ACCEPT, Set.of(COOKING, CANCELED), // 后厨没接单前可以整单取消 COOKING, Set.of(SERVED, CANCELED), // 制作中可以取消但要留操作日志 SERVED, Set.of(PAID), // 已出餐只能结账 PAID, Set.of(), CANCELED, Set.of() ); public static void check(OrderStatus from, OrderStatus to) { if (!FLOW.getOrDefault(from, Set.of()).contains(to)) { throw new BizException(订单状态不允许从 from 变到 to); } } }并发上要防的是前台两个人同时点「结账」会重复生成支付记录。做法是乐观锁update t_order set status 40 where id ? and status 30看影响行数返回 0 就说明有人抢先了直接提示「该桌已结账」。5. 避坑与排查上线前最容易翻车的五处细节5.1 桌台与订单状态类现象一同一桌扫了两次码后厨收到两张一样的单菜做了两遍。原因通常不是幂等没做而是sessionId生成的位置错了——如果放在前端localStorage里换台设备、清一次缓存就是新的会话服务端根本不知道这两次属于同一桌。解决办法是把开台动作收到服务端GET /api/table/{id}/session由后端查活跃会话有就返回已有的没有才新建前端只负责把返回的sessionId带在后续请求头上。现象二加菜之后主单金额和明细之和对不上差几毛。原因是金额在两处独立计算主单用前端传的合计明细用后端的单价乘数量一旦有规格加价或者折扣两边就分叉。修法是只算一次主单金额永远等于明细求和落库前加一句断言不相等就直接抛异常。宁可当场报错也不要让一张错账流到对账环节。5.2 长连接与局域网部署类现象三后厨大屏用着用着就不出新单了F5 刷新立刻恢复正常。八成是 Nginx 把空闲连接掐了而前端既没心跳也没重连。要同时改三处Nginx 配置里把proxy_read_timeout和proxy_send_timeout调到 300 秒以上proxy_set_header Upgrade $http_upgrade和Connection upgrade补上前端加 30 秒心跳重连用指数退避。只改其中一处问题会以更隐蔽的方式回来。现象四平板输内网 IP 打不开点餐页电脑上一切正常。第一查 dev server 是否只监听127.0.0.1加--host 0.0.0.0第二查平板和电脑是不是同一个网段很多公司 WiFi 开了终端隔离同网段也互相 ping 不通第三查电脑防火墙是否放行了 5174 端口。这三步按顺序排查比上来就改配置快得多。5.3 打印与外设类现象五小票机打出来的中文是乱码英文和数字正常。原因基本锁定在编码热敏小票机默认代码页是 GBK而项目里按 UTF-8 拼的字符串。修法是两条路要么在 ESC/POS 初始化指令里显式切换到 UTF-8 代码页0x1C 0x26之类具体看机型手册要么在发送前把字符串按 GBK 转一次字节数组。后者兼容性更好代价是生僻字会丢。另外记得先发0x1B 0x40初始化指令清空打印机缓冲否则上一单没打完的残留会被带进新单打出来是半张错位的纸。6. 二次开发把通用模板改成答辩时能讲清楚的那一个这套代码的骨架是通用的直接拿去答辩会遇到一个尴尬功能太全反而讲不出重点。我的建议是砍掉一半只留一条你自己能从头讲到尾的链路然后在上面加一个别人没有的点。三个改造成本低、效果明显的方向。第一是预点单顾客在到店路上先扫码选菜到店后服务员一键转成正式单。这个改动只涉及t_order加一个pre_order标记和一条状态流转但业务故事完整面试官和老师都爱听。第二是估清联动后厨点某个菜卖完了顾客端实时置灰。改动点在菜品表加stock字段下单时用update ... where stock qty扣减配合 WebSocket 推一条DISH_SOLD_OUT消息几十行代码但把「实时」这两个字讲透了。第三是结账拆单一桌人 AA按明细勾选分别结账。这个改动主要在前端后端加一个「按明细 id 结账」的接口能体现你对订单状态机的理解。改造方向主要涉及文件风险点预点单order/OrderService、guest-h5菜单页预点单超时未到店要不要自动取消得想清楚估清联动dish/DishService、kds-web推送处理扣减要用条件更新不能先查后改结账拆单order/OrderController、admin-web结账页部分结账后主单状态怎么表示别留半开状态改完之后怎么验证比改本身更重要。我一般的顺序是先用 Postman 把下单接口连打十次同一个clientToken看库里是不是只有一单再把 Redis 停掉看幂等是否退化到数据库唯一索引兜底报错是否友好接着拔网线三十秒再插回去看后厨大屏能不能自己恢复最后把平板横竖屏各转一次看购物车是否还在。这四步走完基本能覆盖毕设答辩现场可能出现的所有意外。从那以后我每接一个点餐类的小系统都先把「桌台会话怎么开」和「幂等键放哪」这两件事在白板上画一遍画不清楚就不动手写页面。页面是最容易返工的部分状态模型不是改一次要动全身。这套源码的目录和 SQL 都在压缩包里按第 3 章的命令跑一遍跑不通就翻回第 5 章对着现象找希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询