智能点餐系统开发实战:Spring Boot + Vue 构建完整Web点餐平台 简介这是一份面向计算机专业毕设与课程作业的智能点餐系统完整源码包定位为学习型项目参考涵盖了用户点餐、菜单管理、订单处理、支付集成、库存管理、数据分析及后台管理等典型模块适合需要快速理解真实业务系统结构、提升前后端开发能力的开发者。压缩包共259个文件主要包括102个Java源码、81个XML配置与布局文件、60张PNG界面原型图以及Gradle构建脚本、依赖库等资源整体体积仅1.73MB目录清晰、便于按模块检索。当前已有109人学习或下载这一资料。通过研读代码可掌握Android客户端与服务器端的数据交互方式、数据库表设计、接口调用流程等关键实践还能借鉴工程分层、异常处理与资源整合的常规做法是一份能直接启发毕业设计与课设实现的参考资料。1. 智能点餐系统是什么先想清楚再动手别一上来就写代码看到「智能点餐系统」这个标题大部分人的第一反应是「点餐加个推荐算法」但真正让这类毕设和课程作业翻车的通常不是算法而是订单状态对不上、金额算错、并发出问题这些基础环节。很多 A 同学在开题时把「智能」两个字想得太大最后交付时却被导师问一句「你的系统哪里有智能」答不上来。其实这个标题的合理预期是一个能跑通点餐、下单、支付回调、订单流转、菜品推荐的完整 Web 系统「智能」体现在推荐、库存提醒和数据分析这三个可解释、可演示的模块上而不是蹭 AI 热点去搞什么深度学习识别菜品。这篇内容适合两类人一类是要交毕设或课程作业、需要在一到两个月内拿出可演示系统的同学另一类是已经在写 CRUD 但想了解订单状态机、金额精度和推荐接口怎么落地的开发者。本文会按「先拆模块、再定技术、后写代码、最后排坑」的顺序用可复现的方式讲清楚这套系统从零到一的过程。你不需要有高深算法基础但需要能把 Spring Boot 项目跑起来、会基本的 SQL 和 Vue 语法剩下的我会给出完整路径。2. 技术选型与模块拆分用 Spring Boot Vue 把「智能」落成可交付的课设2.1 为什么选 Spring Boot Vue而不是 SSM 或纯 JSP智能点餐系统作为毕设和课程作业最常见的提交形式选型的第一标准不是「技术新」而是「能在演示环境稳定跑起来、代码量可控、老师能看懂」。Spring Boot 比 SSM 省掉了大量 XML 配置内嵌 Tomcat打成一个 jar 就能跑对不熟悉服务器部署的同学最友好。前端用 Vue Element UI 是一套非常成熟的中后台方案表格、表单、弹窗组件齐全做管理端和点餐端都顺手。如果你只写过 JSP也不是不能用但演示时页面样式和交互感会明显吃亏给老师的印象分就不一样。后端结构建议按模块分包而不是按三层架构平铺。具体来说controller 只做参数接收和结果封装service 写业务规则mapper 用 MyBatis-Plus 做单表 CRUD复杂查询写 XML。推荐模块独立成一个 service不要和订单业务耦合。这样的好处是你回答老师提问「系统怎么设计的」时能清晰说出每个模块的职责而不是含混地说「就是 MVC」。项目骨架可以按下面的模块来拆桌台管理桌号、状态空闲/占用、容纳人数菜品管理分类、名称、价格、图片、库存、销量订单模块购物车、下单、支付回调、订单状态流转推荐模块基于菜品关联热度或用户历史行为的 TopN 推荐数据看板菜品销量排行、营业额趋势、库存预警这里有个取舍要注意如果课设周期只有三四周砍掉数据看板保留前四个模块就够了。推荐模块必须要做因为「智能点餐」的「智能」主要靠它撑起来。2.2 数据库设计五张核心表重点在订单表和订单明细表数据库设计是这套系统的地基。很多同学喜欢把订单菜品直接存成一个字符串字段比如dish_names鱼香肉丝x2,米饭x1这样做演示时没问题但老师一问「怎么统计菜品销量排行」就答不上来了。正确的做法是把订单拆成两张表订单主表存总额、状态、桌号、下单时间订单明细表存每个菜品 ID、单价、数量、小计金额。这两张表通过 order_id 关联统计销量时直接 group by dish_id 聚合明细表。核心表结构见下面 SQL字段命名统一用下划线风格注释写清楚。这里给到可直接执行的建表语句CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号格式yyyyMMddHHmmss4位随机, table_id bigint NOT NULL COMMENT 桌台ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额单位元, status tinyint NOT NULL DEFAULT 0 COMMENT 订单状态0已下单 1已支付 2制作中 3配送中 4已完成 5已取消, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL COMMENT 支付时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_table_id (table_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE order_items ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL COMMENT 订单主表ID, dish_id bigint NOT NULL COMMENT 菜品ID, dish_name varchar(64) NOT NULL COMMENT 冗余菜品名称防止菜品表被删后无法追溯, price decimal(10,2) NOT NULL COMMENT 下单时单价快照, quantity int NOT NULL COMMENT 数量, subtotal decimal(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;建表时有三个设计细节需要特别说明。第一order_items 表里冗余了一份 dish_name 和 price 快照这在毕设答辩时是加分项因为你能回答「菜品改价后历史订单怎么追溯」这个问题。第二订单状态用 tinyint 存数字而不是字符串配合状态机代码一起解释逻辑更严谨。第三订单编号用唯一索引而不是主键自增这是为了让订单号看起来像真实业务编号后面排障时也方便关联日志。菜品表的设计要比你想的更细一点。除了 id、name、price、category_id、image 之外建议加上stock库存字段和sold_count销量字段。这两个字段会在库存校验和推荐模块里用上。库存字段初始值从 1 到 999 都可以演示时不用太纠结数据但字段一定要有不然老师问「库存怎么扣减的」会答不上来。2.3 模块边界把「智能」拆成三个可演示的子功能「智能」这个词最容易做成黑匣子。我的建议是把它拆成三个可解释、可演示的能力推荐、库存预警、销量分析。推荐的落地做法是给用户展示「猜你喜欢」或「本店热卖」列表库存预警是在菜品库存低于阈值时后台管理系统展示红色提示销量分析是一个简单的排行榜接口按日、周、月聚合订单明细返回前 N 名菜品。这三个能力的技术难度梯度很清晰。销量分析最简单一条 group by SQL 就能搞定。库存预警需要一个定时任务或在下单后触发检查。推荐则可以做两个版本协同过滤算法的 MVP 版本或者简化版的「基于菜品共现频率」推荐。这里特别提醒毕业论文里可以写协同过滤、矩阵分解这些词但代码实现尽量用简化版本。因为答辩老师大概率会问「这个算法的原理是什么」如果你实现了复杂的 SVD 但解释不清楚反而减分。简化版「用户点了 A 菜品同时常点 B 菜品则给新用户推荐 B」的逻辑既好解释又有效果。接口设计上后端要提供下面这些 REST 接口模块接口路径方法说明点餐/api/order/createPOST创建订单点餐/api/order/pay/callbackPOST模拟支付回调推荐/api/recommend/topnGET返回推荐菜品列表菜品/api/dish/listGET分页查询菜品数据/api/stats/rankGET菜品销量排行接口设计有个关键原则推荐接口的入参只有user_id和limit两个参数不要暴露算法细节内部实现无论怎么改前端都不受影响。这样你在后期调优推荐策略时不需要改动控制层。3. 两条核心代码链路点餐下单闭环与「智能」推荐接口的实现3.1 点餐下单从购物车到订单状态机的完整实现订单创建是整个系统最核心的代码链路也是面试和答辩时最容易展开讲的部分。我先给出一个可用的 Service 层代码逻辑重点在于事务控制、状态流转和库存扣减。下单流程分四步校验桌台和菜品 → 计算金额 → 扣减库存 → 生成订单及明细。下面这段代码是下单服务的关键实现使用 MyBatis-Plus 的 LambdaQueryWrapper 做条件查询Override Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 1. 校验桌台状态 TableInfo table tableMapper.selectById(dto.getTableId()); if (table null || table.getStatus() ! 0) { throw new BizException(桌台不存在或已占用); } // 2. 组装明细并试算金额 ListOrderItem items new ArrayList(); BigDecimal total BigDecimal.ZERO; for (OrderItemDTO item : dto.getItems()) { Dish dish dishMapper.selectById(item.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new BizException(菜品不存在或已下架); } // 库存扣减前的可用校验 if (dish.getStock() item.getQuantity()) { throw new BizException(菜品[ dish.getName() ]库存不足); } BigDecimal subtotal dish.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity())); total total.add(subtotal); OrderItem orderItem new OrderItem(); orderItem.setDishId(dish.getId()); orderItem.setDishName(dish.getName()); orderItem.setPrice(dish.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setSubtotal(subtotal); items.add(orderItem); } // 3. 生成订单主记录初始状态为「已下单」 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setTableId(dto.getTableId()); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 4. 批量插入明细并扣减库存 for (OrderItem item : items) { item.setOrderId(order.getId()); } orderItemMapper.insertBatch(items); dishMapper.deductStock(dto.getItems()); // 5. 将桌台置为占用状态 tableMapper.updateStatus(dto.getTableId(), 1); return new OrderVO(order.getId(), order.getOrderNo(), total); }这段代码有几个关键参数和逻辑解释。Transactional(rollbackFor Exception.class)表示任何运行时异常都会回滚整个事务防止出现「订单创建成功但库存没扣」的不一致状态。金额计算用的是 BigDecimal 而不是 double这是金额精度问题的根本解法后面避坑章节会展开讲。步骤 4 的库存扣减单独抽了个deductStock方法内部用的是UPDATE dish SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}这是典型的乐观锁防超卖写法代码里加了受影响行数校验返回 0 就抛出库存不足异常。订单编号生成我一般会做一个工具方法格式是yyyyMMddHHmmss加四位随机数保证并发下不重复。用DateTimeFormatter格式化当前时间再拼上ThreadLocalRandom生成的 1000 到 9999 的随机数即可。不要用 UUID订单号太长演示和日志检索都不方便。3.2 模拟支付回调与库存「回滚」逻辑毕设系统通常不会真接支付宝和微信支付因为需要商户号资质。常见做法是做一个模拟收银台页面前端点「确认支付」后后端直接调/api/order/pay/callback接口把订单状态从未支付改为已支付。这个模拟接口虽然简单但状态机的校验逻辑不能省。下面给出支付回调的实现Override Transactional(rollbackFor Exception.class) public void payCallback(PayCallbackDTO dto) { // 1. 根据订单号查询 Order order orderMapper.selectByOrderNo(dto.getOrderNo()); if (order null) { throw new BizException(订单不存在); } // 2. 重复支付判重只有状态为「已下单」才能流转到「已支付」 if (order.getStatus() ! 0) { throw new BizException(订单状态异常当前状态 order.getStatus()); } // 3. 更新状态并记录支付时间 order.setStatus(1); order.setPayTime(LocalDateTime.now()); orderMapper.updateById(order); // 4. 桌台仍保持占用状态不需要释放 }这里的核心是状态机校验订单只能从已下单流转到已支付。如果前端重复点击支付按钮或回调接口被重复调用第二次请求会因为状态不是 0 而抛出异常。异常信息里带上当前状态值方便排障时定位是「哪一步状态不对」而不是笼统的「支付失败」。订单取消和支付超时是另一组需要处理的状态流转。常见做法是用户未支付时订单允许取消取消后恢复库存并把桌台释放为空闲。我把这个逻辑放在订单模块里单独一个方法cancelOrder内部做两件事把订单状态改成「已取消」把订单明细对应的菜品库存加回去、桌台状态改回空闲。这个「回滚」逻辑在答辩时经常被问到务必实现完整。烹饪和配送这两个状态在课设里可以简化处理提供一个「订单状态推进」接口由管理员或商家端手动点击「开始制作」「完成配送」按钮来推进状态。如果不想做过多的按钮可以直接在做数据填充时把订单前置成「已完成」状态演示排行榜时用。核心演示流程保持在「下单 → 支付 → 状态流转 → 数据可见」这条链路上就够了。3.3 推荐模块基于菜品共现频率的 TopN 推荐算法原理 代码推荐模块是「智能」两字的直接体现也是你这套系统区别于普通 CRUD 的关键。我强烈推荐实现「基于菜品共现」的简化版协同过滤原因有三不需要用户画像数据冷启动问题不明显代码量只有几十行答辩时能讲清楚。算法的核心思路是统计「有多少订单同时包含菜品 A 和菜品 B」订单里出现的次数越多说明这两个菜品的关联度越高。当用户点了菜品 A 时按关联度从高到低推荐其他菜品。下面给出核心统计 SQL 和 Java 实现// 第一步查出所有订单包含的菜品ID集合 ListMapString, Object orderDishes orderItemMapper.selectOrderDishMapping(); // 每行数据order_id, dish_id // 第二步构建共现矩阵使用 HashMap 嵌套计数 MapLong, MapLong, Integer coMatrix new HashMap(); for (MapString, Object row : orderDishes) { Long orderId (Long) row.get(order_id); Long dishId (Long) row.get(dish_id); coMatrix.computeIfAbsent(dishId, k - new HashMap()); // 这里略去了同订单内两两累加的遍历细节 // 完整实现是先收集同一订单的 dishId 列表再做双重循环累加 } // 第三步用户点过的菜品作为输入取关联度 TopN ListLong userDishIds userHistoryMapper.selectRecentDishIds(userId); MapLong, Integer scoreMap new HashMap(); for (Long dishId : userDishIds) { MapLong, Integer related coMatrix.getOrDefault(dishId, Collections.emptyMap()); for (Map.EntryLong, Integer entry : related.entrySet()) { scoreMap.merge(entry.getKey(), entry.getValue(), Integer::sum); } } // 排除用户已经点过的菜品按分数排序取前 N 个这段代码的完整版里第二步的「同订单内两两累加」需要额外实现。具体写法是按订单 ID 分组拿到每个订单里的所有菜品 ID 列表然后对这个列表做双重循环coMatrix[dish[i]][dish[j]]加一且跳过i j的情况。这个双重循环的量级在演示数据下非常小哪怕你灌了 5000 条订单数据也就是一两秒的事不需要额外优化。推荐接口的关键参数是limit默认取 5 个。如果用户没有历史订单可以用「销量最高的前 N 个菜品」作为兜底推荐这就解释了为什么菜品表要有 sold_count 字段。推荐接口的内聚性很好后面想换成基于用户相似度的算法只需要替换 service 内部实现Controller 层完全不用动。这就是把「智能」模块独立拆出来的价值。4. 三个必调参数与一组关键配置金额精度、Tomcat 并发与前后端联调4.1 金额计算必须用 BigDecimal三个典型场景演示金额精度问题是智能点餐系统里最容易出 bug 又最不容易被发现的坑。首选建议是后端所有金额相关的 Java 变量类型都用BigDecimal数据库字段用decimal(10,2)而不是float或double。为什么 double 不行因为0.1 0.2在二进制浮点数下等于0.30000000000000004如果订单里有两个菜品分别是 9.9 元和 0.2 元总额会显示成 10.100000000000001 元非常影响演示效果。我一般会在工程里放一个MoneyUtil工具类封装金额计算的标准写法public class MoneyUtil { /** 统一使用这个 scale 做金额精度控制 */ public static final int SCALE 2; /** 加法 */ public static BigDecimal add(BigDecimal a, BigDecimal b) { return a.setScale(SCALE, RoundingMode.HALF_UP) .add(b.setScale(SCALE, RoundingMode.HALF_UP)); } /** 金额格式化去掉末尾多余的0但保留两位小数 */ public static String format(BigDecimal amount) { return amount.setScale(SCALE, RoundingMode.HALF_UP).toString(); } }在这个工具类的使用中RoundingMode.HALF_UP是四舍五入模式。这里有个实际业务决策要说明单价乘数量的时候可能存在 2 位以上小数吗菜品的单价一般精确到分数量是整数乘积不会超过 2 位小数。但如果你做了折扣、满减、优惠券这些营销功能金额精度问题就会明显放大。我的建议是毕设里如果要加优惠模块统一用「满 100 减 10 元」这种整型优惠不要搞 88 折这种会产生无限循环小数的规则省去一堆验证的麻烦。前端展示金额时Vue 里不要直接用{{ item.price * item.quantity }}这种 JS 浮点乘法会出现19.899999这样的渲染结果。正确做法是后端在返回 VO 时就把subtotal和totalAmount算好前端只负责展示字符串。如果你一定要在前端计算可以用Math.round(val * 100) / 100先做一次处理但这是临时方案不推荐。4.2 下单接口的超时与并发参数T-SQL 之外的两个配置点毕设系统不需要高并发但答辦演示时如果老师同时打开多个页面操作就会出现连接超时或 500 错误非常影响观感。你需要先确认三件事本地 MySQL 的连接池上限、Tomcat 的最大线程数、以及 MyBatis 的连接超时设置。默认配置下HikariCP 连接池的 maximum-pool-size 是 10Tomcat 默认线程数是 200看起来够用但实际会因为慢查询把连接占满。我建议在application.yml里显式配置这些参数而不是用默认值spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 mvc: async: request-timeout: 60000 server: tomcat: threads: max: 100 min-spare: 20 accept-count: 200参数说明maximum-pool-size设为 20是因为本地 MySQL 默认最大连接数是 151留出余量connection-timeout设 30 秒避免秒杀式的请求在连接等待时报错。Tomcat 线程数降到 100对课设系统来说不会有性能影响但排队策略更清晰。accept-count设 200 表示队列里最多能排 200 个请求超出后请求会被拒绝并快速返回错误而不是无限堆积导致浏览器转圈。有一个和连接池强相关的坑如果你在代码里使用了Thread.sleep(5000)模拟耗时操作HikariCP 的连接会被占用 5 秒20 个连接 5 秒内满后续请求全部等待。我见过不少同学的系统在演示时卡死就是因为某个接口里加了无意义的 sleep 来「模拟网络延迟」。把这类无意义 sleep 全部删掉是比较好的习惯。4.3 前后端联调三个关键配置跨域、时间格式、接口前缀前后端分离的项目联调阶段最容易在三个细节上卡住。第一是跨域。Vue 开发服务器默认跑在 8080 端口Spring Boot 跑在 8081 端口前端请求后端必然跨域需要在后端写一个 CORS 配置类。第二是时间格式。Java 后端的LocalDateTime默认序列化成2025-01-15T10:30:00Vue 的 Element UI 的 date-picker 不认这个格式展示会出错。第三是接口前缀。前后端要统一约定请求路径前缀/api避免将来部署时 Nginx 转发规则混乱。跨域配置可以直接用 Spring Boot 的 WebMvcConfigurer 来实现Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }时间格式统一用JsonFormat注解public class OrderVO { JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime; }这里的timezone GMT8很重要很多同学不加这个参数结果数据库存的是北京时间后端返回的也是北京时间但前端显示却少了 8 小时。原因是 Java 默认序列化用的是 JVM 时区不是东八区。显式指定GMT8可以彻底规避这个和本地环境相关的时区问题。接口前缀的约定建议写成开发规范文档的一部分同一份文档里把字段命名风格驼峰和响应包装结构{ code, message, data }也一并固定下来。5. 避坑清单智能点餐系统最常见的 5 个翻车点含复现场景与修复5.1 翻车点一下单成功后菜品库存变成负数现象在管理端看到某个菜品的库存字段是 -5。原因下单扣减库存的 SQL 写成了没有条件的裸更新例如UPDATE dish SET stock stock - #{quantity}在高并发或重复点击的场景下库存会被扣到负数而不报错。我见过某位同学的代码前端点了两次「提交订单」后端没有做幂等处理两次请求都扣了库存。解决把库存扣减 SQL 改成带条件的乐观锁写法并且用受影响行数做校验。MyBatis-Plus 里可以这么写Update(UPDATE dish SET stock stock - #{quantity}, sold_count sold_count #{quantity} WHERE id #{dishId} AND stock #{quantity}) int deductStock(Param(dishId) Long dishId, Param(quantity) Integer quantity);这段 SQL 的执行逻辑是stock 大于等于本次要扣的数量才会执行否则会影响 0 行。调用方要判断deductStock的返回值如果返回 0 就抛出库存不足异常并让整个事务回滚。这是最基础且有效的防超卖写法答辦时解释起来很清晰。5.2 翻车点二金额显示成 9.999999 或 13.999999999现象订单总额显示为10.099999999或者详情页的小计金额和总数对不上。原因代码里用了 double 或者 float 做金额累加。9.9 0.2在二进制浮点运算下的结果不精确这是语言层面的限制和 MySQL 无关。解决全局搜索项目中所有double和float类型变量凡是和金额相关的全部改为BigDecimal。数据库字段类型确认是decimal(10,2)。如果你用了 Lombok实体类里金额字段的 getter 返回值类型是BigDecimal前端拿到的 JSON 也会变成字符串10.10而不是数字这是正常的。我见过有同学因为前端显示成字符串觉得不对又去转成数字结果精度问题回来记住 JSON 序列化 BigDecimal 成字符串是安全的行为不要人为转回数字。5.3 翻车点三拿不到 userId 导致推荐接口报错现象前端调用推荐接口时后端报user_id不能为空的参数异常。原因很多同学在设计点餐系统时根本没有登录体系前端直接下单推荐接口不知道给谁推荐。或者用户登录后在 localStorage 里存了 userId但刷新页面后 token 过期前端没做重新登录跳转带着值为 null 的 userId 去请求接口。解决在推荐接口里做两级兜底。一级是用户 ID 不存在时不报错而是返回销量最高的菜品列表二级是用户没有任何历史下单记录时也返回销量榜。这样接口永远不会因为参数问题而崩溃演示时即使登录态丢失页面也有数据展示。代码上这样写if (userId null) { return dishMapper.selectTopSold(limit); }这个「降级返回」的思路在答辩时反而会成为亮点因为你可以说「系统对冷启动用户有专门的处理策略」。5.4 翻车点四订单状态流转错乱已取消的订单还能去支付现象订单已经取消但用户在前端仍能发起支付请求后端也成功把状态改成了「已支付」。原因状态机校验只写了「状态等于某值才允许流转」但没有写「状态不等某值则拒绝」。比如支付回调里只判断了状态是否为 0如果订单已取消、状态是 5那么状态既不是 0代码又没有 else 分支抛异常请求就静默通过了。解决所有状态流转操作统一走一个状态机校验工具类。检查点有两个当前状态必须是「允许出发的状态」目标状态必须是「当前状态的下一个合法状态」。一个最小实现private static final MapInteger, ListInteger STATE_MACHINE new HashMap(); static { // 已下单 - 已支付 / 已取消 STATE_MACHINE.put(0, Arrays.asList(1, 5)); // 已支付 - 制作中 STATE_MACHINE.put(1, Collections.singletonList(2)); // 制作中 - 配送中 STATE_MACHINE.put(2, Collections.singletonList(3)); // 配送中 - 已完成 STATE_MACHINE.put(3, Collections.singletonList(4)); } public static void checkTransition(Integer from, Integer to) { ListInteger allowed STATE_MACHINE.get(from); if (allowed null || !allowed.contains(to)) { throw new BizException(非法订单状态流转 from - to); } }这段代码把状态流转规则集中放在一个地方避免在 Service 里散落 if-else。这也是代码评审和答辩时能被快速理解的好设计。随手补充一个经验状态机数字含义要固定不要在中途调整数字和状态的对应关系否则历史数据会全部错乱。5.5 翻车点五MySQL 时报错「Data too long for column remark」现象用户在下单备注里输入了很长一段文字提交后后端报 500日志显示Data too long for column remark。原因数据库remark字段设置的是varchar(50)或varchar(100)前端输入框没有做 maxlength 限制。这类错误在演示时非常尴尬因为问题不是代码逻辑而是字段长度校验缺失。解决双重校验。前端在输入框上设置maxlength200后端在 DTO 上加上Size(max 200)注解。数据库字段统一升级到varchar(255)防止意外超长。另一个容易踩同样的坑的字段是「订单备注」和「收货地址」这两个自由度比较高的数据字段长度不要抠得太紧255 是可以接受的长度。后端校验注解加上后即使绕过前端直接调接口也会返回参数校验错误而不是数据库异常。6. 进阶验证用一段测试脚本把整个系统钉在可用状态当你把系统开发完接下来要做的是「把它钉在可用状态」——也就是用自动化脚本验证核心链路而不是在答辦前手工点一遍。手工点菜容易漏掉并发场景和状态边界我用 python 脚本模拟真实用户操作已经成了交付前的固定动作。下面这段脚本用 requests 库模拟完整流程下单两个菜品 → 模拟支付 → 确认状态推进 → 查询销量榜看数据是否更新。注意脚本里的接口地址请改成你自己的后端地址。import requests import random import time import json BASE_URL http://localhost:8081/api def test_full_order_flow(): # 1. 选两个菜品并下单 dish_list requests.get(f{BASE_URL}/dish/list, params{current: 1, size: 10}).json() dishes dish_list[data][records][:2] order_items [ {dishId: dishes[0][id], quantity: 1}, {dishId: dishes[1][id], quantity: 2} # 第二个菜点2份测数量计算 ] create_resp requests.post( f{BASE_URL}/order/create, json{tableId: 1, items: order_items}, headers{Content-Type: application/json} ) assert create_resp.status_code 200, f创建订单失败: {create_resp.text} order_no create_resp.json()[data][orderNo] # 2. 模拟支付回调 pay_resp requests.post( f{BASE_URL}/order/pay/callback, json{orderNo: order_no} ) assert pay_resp.json()[code] 200, f支付回调失败: {pay_resp.text} # 3. 查询订单状态确认已支付 time.sleep(0.5) # 等待数据库写入完成 order_resp requests.get(f{BASE_URL}/order/detail, params{orderNo: order_no}) data order_resp.json()[data] assert data[status] 1, f订单状态应为已支付(1)实际是 {data[status]} # 4. 校验金额两份0.2元的菜 一份9.9元的菜 expected_total round(dishes[0][price] * 1 dishes[1][price] * 2, 2) assert float(data[totalAmount]) expected_total, f金额不一致: {data[totalAmount]} vs {expected_total} # 5. 触发一次推荐接口 rec_resp requests.get(f{BASE_URL}/recommend/topn, params{userId: 1, limit: 5}) assert rec_resp.status_code 200, 推荐接口调用失败 assert len(rec_resp.json()[data]) 0, 推荐接口未返回数据 print(核心链路验证通过金额、状态、推荐全部正常) if __name__ __main__: test_full_order_flow()这段脚本里的断言很关键。第一步断言创建订单返回 200第二步断言支付回调返回 code 200第三步断言状态恰好是 1第四步断言金额精确等于「1 份菜 A 2 份菜 B」的和。如果中间任何一步失败脚本会打印出响应的 text你可以直接定位是哪一环的问题。脚本里的 expected_total 用round(..., 2)做了一次修正是因为 Python 的浮点乘法和 JAVA 后端返回的 BigDecimal 字符串转 float 后可能有一丝的偏差。这是脚本侧的处理不影响后端精度。除了这条核心链路我建议再补两个边界脚本一个是不存在的桌台下单期望返回业务异常另一个是重复支付同一个订单期望第二次支付失败。把这两个用例也写成断言后答辦前跑一遍三类错误都会被系统拦下。这个测试脚本放在src/test目录旁边即可不需要引入 JUnit直接用 Python 跑。答辦的时候如果老师问「你怎么保证系统稳定」你可以说出「用脚本模拟了核心链路包括异常场景」这比口头说「我测过了」要有说服力得多。如果条件允许再做一个简单的数据初始化脚本插入 20 个菜品、5 个桌台和 50 条已完成的订单历史数据。因为推荐算法是建立在共现矩阵上的没有历史订单数据推荐模块在冷启动时只能返回销量榜等于「智能」能力没有被展示出来。预先灌数据不仅能让你演示时直接看到推荐效果还能顺便验证销量分析模块的 SQL 是否正确。项目的收尾阶段我只建议做一件事把项目重新 clone 到一个新目录按 README 从头跑一遍。从建库、导 SQL、启动后端、启动前端到脚本验证一条线执行完。这个过程往往会暴露你早就忘了的环境依赖问题比如 MySQL 版本、JDK 版本、Node 版本早发现早解决。我自己的经验是这类系统的完成度不在于代码多华丽而在于「演示时不出错」和「被问倒时有话讲」。把状态机校验、金额精度、库存防超卖、推荐兜底这四个点做扎实哪怕推荐算法只是基于共现频率的简化版本已经足够在一群 CRUD 系统里展现出「智能点餐」该有的样子。希望这些流程和踩坑记录能帮你用更少的返工把项目跑通答辦顺利。本文还有配套的精品资源点击获取