Spring Boot构建微信小程序点餐系统:从表设计到活动优惠的完整实践

📅 发布时间:2026/10/21 19:36:22
Spring Boot构建微信小程序点餐系统:从表设计到活动优惠的完整实践
“这套系统做完之后我最大的感受是微信小程序点餐听起来像是个‘低门槛’项目真正落地时你会发现坑几乎全在细节里——从微信登录态维护到库存扣减时序从活动规则设计到并发下单的超卖问题每一个都能让人熬掉几根头发。”如果你正准备做类似“餐厅食堂点餐小程序”的课题或实际项目或者已经有一个半成品但总感觉跑不通、被各种边界问题卡住那这篇文章值得你从头看完。我梳理了一套基于 Java Spring Boot 和微信小程序原生 / uni-app 均可的完整点餐系统设计与实现方案覆盖了从需求拆解、数据建模、接口设计、活动玩法到部署展示的闭环过程。文中所有代码片段均来自我实际调试通过的核心逻辑偏向可直接“抄作业”但更希望你把“为什么这样写”一起带走。1. 项目概述与核心需求拆解1.1 标题背后的真实业务场景先把这个项目标题翻译成大白话一套为餐厅或食堂定制的微信小程序点餐应用后端采用 Spring Boot 提供业务接口小程序端实现菜单展示、下单、支付、订单管理等操作在基础点餐之上系统还需要支持类似“限时折扣”“满减优惠”“节日特惠”等美食活动用来提升点餐率和客单价。源码、开发文档、运行视频和讲解视频是为了便于二次开发和交付验收。这类项目在课程设计、毕业设计、企业内部培训以及小型餐饮信息化改造中非常常见。它本身的技术难度适中但胜在业务链路完整天然包含小程序登录、数据库设计、缓存使用、接口安全、并发控制、文件上传菜品图片等高频开发场景非常适合作为 Spring Boot 综合实战练习。而对于真正的餐饮门店这套系统也可以快速变成一套可运行的点餐工具投入成本远低于采购大型餐饮管理软件。1.2 关键角色划分与功能全景从使用者的角度系统可以拆成三类角色顾客小程序端、商家后台管理端、系统管理员可选。顾客端主要需要三类能力看菜、下单、追订单商家端则需要维护菜品、库存、订单状态、活动配置。为了不至于一开始就陷入“什么都想实现但什么都没做深”的误区我在规划时强制划分了 MVP最小可行产品与可选增强功能。基础版本一定要跑通以下闭环顾客浏览菜品分类与详情选择商品加入购物车提交订单并完成支付可用模拟支付商家在管理后台接单、出餐、完成订单顾客查看订单状态和历史记录在美食活动方面第一版不需要支持“满减叠加优惠券再叠加会员折扣”这种复杂规则。只需要定义一个活动类型、活动时间、参与菜品范围和优惠规则比如满 30 减 5或者指定菜品打 8 折由后端在计算订单金额时统一处理即可。先把主链路做稳定再考虑规则引擎般的灵活扩展这是我反复强调的原则。1.3 为什么是 Spring Boot 微信小程序先说微信小程序这一端。相比原生 App小程序无需安装、门槛低、易分享非常契合“到店扫码点餐”和“食堂临时用餐”的场景。小程序提供的wx.logincode2Session能力可以快速获取用户唯一标识openid后端无需自建用户账号体系这对中小餐饮门店来说是巨大便利。后端选择 Spring Boot 则是基于几个现实考量生态成熟几乎任何你需要的中间件都有现成 starter和微信支付、对象存储、消息推送等服务的 SDK 集成资料丰富打包部署java -jar简单一台普通服务器即可。同时Spring Boot 的自动装配和分层架构非常容易把“点餐系统”这种典型业务组织得干净整洁——Controller 管入参出参Service 管业务逻辑Mapper 管数据库访问。整套系统我建议采用单体应用结构不要在一开始就引入微服务和各种消息中间件。单体应用调试方便、部署简单、事务控制直接完全足够支撑食堂或中小餐厅的并发量。等真的有性能瓶颈再按模块拆分也不迟。2. 系统架构与数据库建模2.1 分层架构设计实际项目中我采用了经典的五层结构前端展示层微信小程序、网关/控制层Controller、业务逻辑层Service、数据访问层Mapper/DAO、基础设施层MySQL Redis 对象存储。严格让 Service 层承载所有业务规则Controller 只做参数接收和结果封装。有一个小技巧所有接口统一返回ResultT结构包含code、message、data三个字段。这样小程序端处理响应时只需要写一个公共的请求封装方法省去大量重复判断。权限控制方面小程序端请求需要校验登录态。实际上不需要依赖 Spring Security 那么重的框架自定义一个拦截器从请求头获取token然后去 Redis 中查openid查不到就返回401让小程序重新登录。商家端后台则单独用一套 Session 管理管理员账号避免和小程序用户混在一起。2.2 核心数据表设计这部分是很多人容易忽略但最重要的环节。表设计不合理后面写功能会一直别扭。下面列出我最终采用的表结构字段已经过精简但保持了关键业务语义。用户表字段名类型说明idbigint主键openidvarchar微信小程序用户标识唯一nicknamevarchar用户昵称avatarvarchar头像路径phonevarchar(11)手机号可选绑定create_timedatetime首次登录时间菜品表字段名类型说明idbigint主键category_idbigint分类 IDnamevarchar菜品名称imagevarchar图片路径pricedecimal(10,2)原价stockint当日库存-1 表示不限量descriptionvarchar文字描述statustinyint1 上架 / 0 下架create_timedatetime创建时间订单表字段名类型说明idbigint主键order_novarchar业务订单号唯一user_idbigint用户 IDshop_idbigint店铺/食堂 IDtotal_amountdecimal(10,2)商品总金额discount_amountdecimal(10,2)优惠金额pay_amountdecimal(10,2)实付金额statustinyint0 待支付 / 1 已支付 / 2 已接单 / 3 制作中 / 4 已出餐 / 5 已完成 / 6 已取消remarkvarchar用户备注create_timedatetime下单时间订单明细表字段名类型说明idbigint主键order_idbigint订单主键dish_idbigint菜品 IDdish_namevarchar菜名快照防止菜品修改后历史订单错乱dish_imagevarchar图片快照pricedecimal(10,2)下单时单价quantityint份数subtotaldecimal(10,2)小计美食活动表字段名类型说明idbigint主键titlevarchar活动名称typetinyint1 满减 / 2 折扣 / 3 直减start_timedatetime开始时间end_timedatetime结束时间thresholddecimal(10,2)门槛金额满减活动用discount_amountdecimal(10,2)优惠金额满减活动用discount_ratedecimal(3,2)折扣率折扣活动用dish_idsvarchar参与菜品 ID 列表逗号分隔为空表示全场statustinyint0 草稿 / 1 启用 / 2 停用在第二版中我增加了activity_rule字段用 JSON 存放更灵活的自定义规则但第一版承载复杂 JSON 解析反而增加出错概率因此这里采用了显式字段方案。还有一个值得强调的设计订单明细表必须做“快照”把下单时的菜名、价格、图片冗余存储下来。否则商家后台改了价格或者删除菜品历史订单会变得面目全非用户翻旧订单的时候会产生大量售后问题。2.3 数据一致性与索引优化点餐系统的数据一致性要求比普通展示型项目高很多。我使用 MySQL 默认的InnoDB引擎事务隔离级别是REPEATABLE READ并通过悲观锁或乐观锁解决库存超卖。最简单有效的方式是UPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity}这行 SQL 本身是一种原子操作只要执行影响行数为 1 就说明扣减成功。它天然避免了多人在同一时刻重复扣减同一份库存的问题比“先查后改”安全得多。在索引设计上订单表需要建立user_id create_time联合索引以便查询“我的订单”订单明细表对order_id建索引菜品表对category_id建索引。其它像openid直接建唯一索引避免重复注册。Redis 方面缓存热点菜品列表和首页轮播图。需要注意缓存一致性商家修改菜品或价格后需要主动删除对应缓存让下次请求重新回源数据库。千万不要图省事设置“永不过期”否则用户端看到的菜单会一直停留在旧数据上。3. 小程序端核心功能实现3.1 登录态与用户体系小程序登录的流程是前端调用wx.login获取临时code传给后端的/api/auth/login后端拿着 code 调用微信接口获取openid和session_key。然后后端生成一份自定义 token可以直接生成 UUID以 openid 为 value 存入 Redis设置过期时间比如 7 天再将 token 返回到小程序端。后续所有请求在小程序端通过请求拦截器自动加上Authorization: Bearer token这个 header。后端拦截器解析 token从 Redis 取出 openid 和用户 ID放进ThreadLocal中方便 Service 层随时获取当前用户。这个方案比直接把 openid 下发到小程序端更安全因为小程序端只是持有 token并没有拿到微信接口的敏感信息。但需要注意一个坑Redis 中的 session 过期时间要定时续期可以在拦截器里判断“当前剩余过期时间小于某阈值”时执行一次expire这样用户只要一直在使用就不会被踢下线。3.2 菜品浏览与购物车小程序首页通常分为顶部轮播图、分类切换栏和菜品瀑布流。分类数据可以一次性拉取菜品列表则根据当前选中的分类动态请求接口。为了减少请求次数菜品列表可以一次性返回当前分类下的所有菜品包含价格、月售、图片和描述。购物车数据结构并不需要单独维护在数据库里直接放在小程序端全局data中即可使用storage做本地持久化。提交订单时再把购物车数据完整塞给后端由后端校验菜品当前是否上架、库存是否充足、价格是否匹配。购物车的作用仅是“预选”真正的规格校验必须以后端计算为准。关于价格校验提醒一句前端不要自己计算实付金额再传给后端。正确做法是前端只传[{dishId, quantity}]后端从数据库读取菜品当前价格并计算总金额这样杜绝了用户篡改请求价格的可能性。我调试过程中曾经遇到有人通过抓包修改金额实现“白嫖”后来强制改为后端计价之后这个问题彻底消失。3.3 下单与模拟支付流程下单流程我建议这样走用户确认购物车点击“去结算”后端收到请求后先校验商品状态和库存。校验通过后生成订单状态为待支付同时扣减库存。如果选择模拟支付直接调用后端“模拟支付成功”接口将订单状态置为已支付。如果对接真实微信支付则后端调用下单接口生成预付单返回wx.requestPayment所需参数前端拉起收银台支付成功后微信服务器异步通知后端回调接口更新订单状态。这里有个非常经典的时序问题如果步骤 2 扣减了库存但用户一直不支付那么库存会被长期占用。解决方案是设置“订单过期时间”通常 15 分钟过期后自动取消订单并回补库存。最简单的方式是使用 Spring 的Scheduled定时任务扫描“待支付超过 15 分钟”的订单批量取消并回补库存。在真正的高并发场景下会更倾向于使用延时队列或延迟消息但对单体项目来说定时扫表是完全够用的。3.4 订单状态机设计订单状态流转不能随意跳转。我用一个枚举来管理状态机public enum OrderStatus { UNPAID(0, 待支付), PAID(1, 已支付), ACCEPTED(2, 已接单), MAKING(3, 制作中), READY(4, 已出餐), COMPLETED(5, 已完成), CANCELLED(6, 已取消); }允许的状态变更包括待支付 → 已支付 / 已取消已支付 → 已接单 / 已取消已接单 → 制作中制作中 → 已出餐已出餐 → 已完成。商家后台每执行一步操作都需要校验当前状态是否在允许变更的列表中。这种设计避免了好几个人同时操作系统时把订单状态搞乱的情况而且在回溯问题时很清晰。3.5 美食活动模块的落地玩法“美食活动”是这个项目的特色也是我花了较多心思的地方。考虑到食堂/餐厅的真实场景我实现了三种基础活动类型满减活动例如满 30 减 5满 50 减 12。计算订单金额时先求商品总价然后判断是否达到门槛达到则在总价上减去优惠金额。折扣活动例如午饭期间指定菜品 8 折。该活动需要按菜品维度参与只有订单中包含参与菜品时才对这部分菜品的价格按折扣重新计算。直减活动例如元旦全场每单立减 3 元规则最简单直接减。活动模块的关键设计在于所有优惠计算必须发生在生成订单时并且需要把“原始总金额”“优惠金额”“实付金额”三个数字全部保存到订单表。这样顾客在前端看到优惠明细时能一目了然后续对账也不会产生歧义。如果同时存在多个活动我采用的策略是“不能叠加”只选择一个对用户最有利的活动。因为在没有成熟优惠规则引擎之前叠加计算很容易出现漏洞比如两个满减活动叠加后出现“实付金额 应付金额”维护成本飙升。当然如果后续有更高要求可以在订单表中增加一个activity_snapshot字段把活动快照 JSON 序列化存进去哪怕之后活动修改了也不会影响历史订单展示。活动生效时间的校验也非常重要。我遇到过为了跑效果把活动时间配错导致全站折扣瞬间失效的问题。因此所有活动查询都需要携带当前时间作为条件执行 SQL 时使用start_time #{now} AND end_time #{now}而不是在 Java 代码里自己比较这样可以避免应用服务器和数据库服务器时间不一致造成误差。4. 后端 Spring Boot 关键实现与接口设计4.1 项目初始化与依赖引入创建一个 Web 项目时我采用 Maven 构建pom.xml中引入必须的依赖spring-boot-starter-webmybatis-plus-boot-startermysql-connector-javaredis、spring-boot-starter-data-redislombokhutool工具库微信支付 SDK 或wechatpay-java能不要的依赖尽量不加保持启动速度和小依赖面。对新手来说依赖冲突最常出现在 Logback 和 Log4j 共存上建议统一使用 Spring Boot 默认的 Logback不额外引入其他日志实现。4.2 统一响应体与全局异常处理所有接口返回统一ResultT结构。我习惯在Result中加入静态工厂方法用于成功/失败返回public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }配套编写一个RestControllerAdvice全局异常处理器对业务异常直接返回Result.error对未预期异常记录日志并返回“系统繁忙”。全局异常处理不只是把代码整洁更重要的是避免把敏感信息比如 SQL 详细异常直接暴露给调用方这在部署到公网服务器时尤其重要。4.3 点餐接口的并发安全实现体验最差的事情莫过于用户选好了菜品下单时提示“库存不足”。为了避免这种现象我在生成订单的核心服务中做了三步处理加锁通过synchronized或分布式锁按userId dishId维度串行化计算。实际上对于单机应用synchronized已经可以解决大部分并发问题不需要引入 Redisson。校验重新从数据库读取菜品价格、库存、状态。扣减执行UPDATE ... WHERE stock #{quantity}如果影响行数不足则回滚事务并抛出业务异常。如果你希望更进一步可以直接在数据库层面使用乐观锁增加version字段或者悲观锁SELECT ... FOR UPDATE。但我的建议是在库存充足且并发量一般的情况下UPDATE 条件判断最稳也最简单。4.4 商家管理后台接口商家端不是一个小程序我更倾向于开发一个基于 Web 的简易管理后台使用 Vue 或纯 Thymeleaf 模板均可。后台提供以下接口POST /admin/dish新增/修改菜品信息上传图片POST /admin/order/accept接单POST /admin/order/status更新订单状态POST /admin/activity创建/编辑活动GET /admin/activity/list活动列表后台的权限隔离要清晰。小程序用户接口和后台管理接口用不同的路径前缀例如/api/user/**和/api/admin/**拦截器根据路径前缀分别判断。绝对不要做“只要带 token 就能访问后台”的操作。我给后台另外设计了一套管理员登录接口使用用户名密码登录生成独立的后台 token并且设置较短的过期时间。4.5 文件与图片存储方案菜品图片是点餐系统的门面处理不好会直接影响用户体验。最省事的方案是将图片上传到云对象存储获取 URL 后保存到数据库。但在本地开发阶段很多同学没有云环境所以可以采用本地上传策略将图片保存到服务器上的某一路径并配置虚拟目录映射到该路径通过域名/IP访问。不过这里有一个踩坑点上传文件必须要限制大小和类型不能只依赖前端限制后端也要做二次校验。我曾在测试时上传了一个几十 MB 的高清图直接导致接口超时。后来增加了spring.servlet.multipart.max-file-size5MB配置并校验文件扩展名和 MIME 类型才把这个问题按住。5. 美食活动模块的算法设计与防坑5.1 活动优惠计算的核心逻辑我写了一个独立的ActivityCalculator组件专门负责活动规则计算避免散落在各个 Service 中。public ActivityCalcResult calc(ListOrderItem items, Activity activity) { // 计算商品总金额 BigDecimal total items.stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal discount; if (activity.getType() 1) { // 满减 if (total.compareTo(activity.getThreshold()) 0) { discount activity.getDiscountAmount(); } else { discount BigDecimal.ZERO; } } else if (activity.getType() 2) { // 折扣 BigDecimal discountPart BigDecimal.ZERO; ListString dishIds Arrays.asList(activity.getDishIds().split(,)); for (OrderItem item : items) { if (dishIds.contains(item.getDishId())) { discountPart discountPart.add(item.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity())) .multiply(BigDecimal.ONE.subtract(activity.getDiscountRate()))); } } discount discountPart; } else { discount activity.getDiscount(); } BigDecimal pay total.subtract(discount); // 下限保护 if (pay.compareTo(BigDecimal.ZERO) 0) { pay BigDecimal.ZERO; } return new ActivityCalcResult(total, discount, pay); }这里有个非常值得注意的点金额计算时一定使用BigDecimal千万别用double或者float。我以前用float计算总价结果出现了类似19.9 * 3 59.699999的诡异数字前端展示出来很难看而且对账会出错。所有金额字段和计算都强制使用BigDecimal并且用setScale(2, RoundingMode.HALF_UP)格式化到分。5.2 活动边界问题处理活动模块常见边缘问题包括活动刚结束用户提交订单时是否还能享受优惠答案是不能。校验时必须使用数据库当前时间而不是请求携带的时间否则用户可以伪造时间。活动限定了指定菜品但用户购物车中只有部分菜品参与。此时只对参与活动的菜品金额计算折扣未参与的正常计价。下单后商家修改了活动力度已创建订单金额不能变化。所以订单表必须存储当时的优惠快照这我前面已经强调过。活动状态变更和菜品上下架的关系如果一个菜品在活动进行中下架了用户下单时直接提示“该菜品已下架”不要再参与计价。5.3 活动效果的简单统计作为“美食活动”模块只做优惠计算还不够。商家一定想知道活动带来了多少订单、多少营收增长。因此我在订单表中增加了activity_id字段记录用户本次下单匹配到的活动 ID。然后后台统计时可以使用SELECT activity_id, COUNT(*), SUM(pay_amount) FROM order WHERE pay_status 1 GROUP BY activity_id;通过这个统计商家可以快速对比活动前和活动后的销量。低成本的统计方式就是增加一个字段不要一开始就引入 BI 系统。6. 微信支付接入要点与常见问题6.1 模拟支付与真实支付的选择如果是课程设计或毕业设计十有八九无法申请正式的微信支付商户号因为需要营业执照、对公账户等资质。最理性的方案是“模拟支付”后端正向流程完全走通只是支付环节用测试接口代替。模拟支付本质上是前端调用后端/api/pay/mock后端校验订单属于当前用户且状态为待支付然后将订单状态置为已支付。配合运行视频演示时评委能够看到完整的“下单→模拟支付→商家接单→出餐”过程业务闭环是完整的。如果是真实商业项目必须走微信支付 V3 接口需要使用商户私钥、平台证书、回调地址等。要注意的关键点是支付回调不能直接当作业务处理结果的最终判定还应该主动调用查询接口验证订单状态防止伪造回调。6.2 微信回调通知的签名验证微信支付回调通知使用Wechatpay-Signature等 header 来体现签名。后端必须验证签名绝对不要直接信任 POST 过来的内容。具体实现步骤获取回调头中的时间戳、随机串、签名类型和签名值。按微信官方文档要求将报文主体及相关参数拼接成待签名字符串。使用微信支付平台公钥对签名做验签。验签通过后再解析报文中的event_type判断是TRANSACTION.SUCCESS后再根据out_trade_no更新订单状态。特别提醒回调处理接口必须是幂等的——即使同一个通知推送多次后端也只能成功更新一次订单状态不会重复增加积分、重复推送消息等。6.3 支付与订单状态的最终一致性这里容易出现一个认知误区小程序前端收到支付成功回调后立即查询订单状态然后以为此刻就是最终状态。实际上支付成功后后端可能还有异步处理逻辑比如生成取餐码、发送通知消息、更新销量统计。如果用户的客户端恰好在某个瞬间查到“待支付”就会产生困惑。我的解决方案是支付成功回调后同步更新订单状态其他事宜放入事件或定时任务异步处理。也就是把“最核心的状态流转”和“周边数据更新”分开前者同步完成后者异步跟进。这样用户在支付后很快就能看到成功反馈。7. 部署与交付源码、文档、视频的完整度把控7.1 本地开发环境搭建部署这套系统前建议把环境列一个清单避免在环境问题上浪费时间JDK 1.8Maven 3.6MySQL 5.7 / MySQL 8.0Redis 5.0微信开发者工具IDEA 或 Eclipse数据库初始化只需要执行项目目录下的sql/init.sql文件它会自动创建库、表并插入一些演示数据比如几个分类、十几道菜品以及一个为期一周的满减活动。本地运行时修改application-dev.yml中的数据源和 Redis 地址即可。7.2 文档编写的经验与技巧界面文档往往是最容易被忽视的部分。我写交付文档时坚持三个原则先写 README包含项目简介、技术栈、启动步骤、目录结构、账号信息。让接手的人 5 分钟内跑起来。再写接口文档在实现编码之前就要确定接口契约可以采用 Swagger 注解生成但最好额外导出一份 Markdown 版本。最后写数据库设计说明每张表字段含义、核心索引、表之间的关系。尤其是活动表和订单表的关联用文字描述清楚。7.3 运行视频和讲解视频如何录制视频是让项目更具说服力的交付物。录制运行视频时建议把摄像头对准电脑屏幕而不是用录屏软件这样可以同步看到操作过程录制视角要稳定不要频繁切窗口。讲解视频则重点围绕“系统架构、数据库设计、核心代码、运行演示”四个部分展开每部分聚焦一个亮点控制在 15-25 分钟为宜。讲代码时不要照着念而是讲清楚“为什么这样写”和“如果换一种方式会有什么问题”。这比单纯贴代码更能体现水平。7.4 服务器部署注意事项如果最终需要部署到云服务器有几个容易踩的坑微信小程序要求所有接口使用 HTTPS 域名并且域名必须备案。开发阶段可以勾选“不校验合法域名”跳过但生产环境必须配置 SSL 证书。微信支付要求回调地址必须是公网可访问的 HTTPS 域名。服务器防火墙需要放行后端端口如 8080但不要开放 MySQL 和 Redis 的公网端口避免被扫库。生产环境 Redis 不要使用默认密码或不设密码建议使用强密码并只绑定内网 IP。定期备份数据库。对餐饮系统来说订单数据就是生命线。我习惯每天凌晨自动执行mysqldump备份到异地目录。8. 常见问题与排查技巧实录8.1 小程序请求后端接口失败原因可能很多但排查顺序建议是先看开发工具控制台报错确认域名是否配置、HTTPS 证书是否有效、后端接口是否正常。一个小技巧小程序开发工具中勾选“不校验合法域名”同时把后端服务监听在0.0.0.0再用局域网 IP 访问能快速定位是不是域名或证书导致的问题。8.2 用户登录时报缺少 code这是极容易犯的错wx.login的code是一次性的如果用完了还要重新调用。不要尝试缓存code多次使用。另外后端调用微信code2Session时需要把AppID、AppSecret配置准确。AppSecret属于敏感信息建议保存在后端配置文件或环境变量中甚至加密存储不能暴露在小程序代码里。8.3 数据库中文乱码建库时统一使用 UTF-8 字符集。连接 URL 中增加characterEncodingutf8参数并配置spring.datasource的编码属性。如果已经出现了乱码先检查 MySQL 库表编码再检查连接参数。解决顺序从底层往上排查不要在小程序端浪费时间。8.4 库存超卖问题如果测试时发现订单数量超过库存数量大概率是扣减逻辑写成了“先查询后更新”中间没有加锁或条件约束。修复方法就是前面说的UPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity}如果发现影响行数为 0说明库存不足需要抛异常。这里还有一个细节即使使用了乐观锁也要避免自旋重试无限循环导致数据库压力升高通常设定最多重试 3 次。8.5 活动计算没有生效检查顺序活动状态是否启用并且当前时间在有效期内。订单商品是否满足活动门槛或活动菜品范围。活动是否被其他代码逻辑悄悄覆盖例如直接用原始价格生成order。确认数据库字段类型正确threshold和discount_amount使用 decimal避免精度损失。我曾经遇到一个“满 30 减 5 不生效”的问题最后发现是因为活动表里threshold在数据库中存的是字符串类型30比较时被强转成整数而商品金额是 decimal两边经过自动转换后结果异常。所以设计表结构时数值字段类型真的要严格定义。8.6 订单状态不同步最典型的是用户支付后商家后台没有收到新订单提醒。常规解法有几种WebSocket 推送、模板消息订阅、定时轮询。对中小型点餐系统我建议在商家后台使用“订单列表 状态标记 定时轮询”的方式每 5 秒请求一次新订单接口发现新订单则播放提示音。这个方案实现简单且稳定不依赖第三方推送渠道。如果对小程序的“订阅消息”有要求也可以在用户支付成功后引导用户授权接收订单状态通知。但这属于锦上添花不是必需项。9. 经验总结与扩展建议这套系统从需求分析到最终交付我花了不少心思在“跑通核心闭环”和“把边界处理干净”上。很多人写点餐系统比写商城还复杂其实往往是因为没有抓住主线——点餐系统的主线就是“菜单 → 购物车 → 订单 → 支付 → 出餐”。其余所有功能包括美食活动、评论、会员积分都应该在这条主线跑通之后进行扩展。我个人在实际操作中比较看重的几个习惯金额计算全部走后端且使用 BigDecimal。订单金额与优惠信息做快照防止历史数据漂移。库存扣减使用条件更新而不是先查后改。所有涉及金额和库存的操作放在同一个事务中避免半成功状态。准备一份可以直接运行的初始化数据方便快速演示。如果你后续想继续深入可以在现有架构上增加“桌台扫码点餐”扫描桌台二维码自动填充桌号后厨打印机接收订单指令或者增加简单的会员积分体系。这些都是点餐系统常见的拓展方向但基本功还是先把手头的闭环打磨稳。最后再分享一个小技巧项目标识、数据库名、包名命名建议统一使用同一组前缀比如项目名diancan数据库db_diancan包名com.example.diancan。前期花五分钟统一命名后面所有配置文件和类名引用就会减少很多低级错误也是保证交付观感的重要细节。这套点餐系统的整体难点并不高只要你愿意把每一步的为什么弄清楚而不是单纯跑通Demo那你收获的不仅是一份源码更是一整套企业级业务系统的设计思维。