“这套系统做完之后,我最大的感受是:微信小程序点餐,听起来像是个‘低门槛’项目,真正落地时,你会发现坑几乎全在细节里——从微信登录态维护到库存扣减时序,从活动规则设计到并发下单的超卖问题,每一个都能让人熬掉几根头发。”
如果你正准备做类似“餐厅食堂点餐小程序”的课题或实际项目,或者已经有一个半成品但总感觉跑不通、被各种边界问题卡住,那这篇文章值得你从头看完。我梳理了一套基于 Java Spring Boot 和微信小程序(原生 / uni-app 均可)的完整点餐系统设计与实现方案,覆盖了从需求拆解、数据建模、接口设计、活动玩法到部署展示的闭环过程。文中所有代码片段均来自我实际调试通过的核心逻辑,偏向可直接“抄作业”,但更希望你把“为什么这样写”一起带走。
1. 项目概述与核心需求拆解
1.1 标题背后的真实业务场景
先把这个项目标题翻译成大白话:一套为餐厅或食堂定制的微信小程序点餐应用,后端采用 Spring Boot 提供业务接口,小程序端实现菜单展示、下单、支付、订单管理等操作;在基础点餐之上,系统还需要支持类似“限时折扣”“满减优惠”“节日特惠”等美食活动,用来提升点餐率和客单价。源码、开发文档、运行视频和讲解视频,是为了便于二次开发和交付验收。
这类项目在课程设计、毕业设计、企业内部培训以及小型餐饮信息化改造中非常常见。它本身的技术难度适中,但胜在业务链路完整,天然包含小程序登录、数据库设计、缓存使用、接口安全、并发控制、文件上传(菜品图片)等高频开发场景,非常适合作为 Spring Boot 综合实战练习。而对于真正的餐饮门店,这套系统也可以快速变成一套可运行的点餐工具,投入成本远低于采购大型餐饮管理软件。
1.2 关键角色划分与功能全景
从使用者的角度,系统可以拆成三类角色:顾客(小程序端)、商家(后台管理端)、系统管理员(可选)。顾客端主要需要三类能力:看菜、下单、追订单;商家端则需要维护菜品、库存、订单状态、活动配置。
为了不至于一开始就陷入“什么都想实现但什么都没做深”的误区,我在规划时强制划分了 MVP(最小可行产品)与可选增强功能。基础版本一定要跑通以下闭环:
- 顾客浏览菜品分类与详情
- 选择商品加入购物车
- 提交订单并完成支付(可用模拟支付)
- 商家在管理后台接单、出餐、完成订单
- 顾客查看订单状态和历史记录
在美食活动方面,第一版不需要支持“满减叠加优惠券再叠加会员折扣”这种复杂规则。只需要定义一个活动类型、活动时间、参与菜品范围和优惠规则(比如满 30 减 5,或者指定菜品打 8 折),由后端在计算订单金额时统一处理即可。先把主链路做稳定,再考虑规则引擎般的灵活扩展,这是我反复强调的原则。
1.3 为什么是 Spring Boot + 微信小程序
先说微信小程序这一端。相比原生 App,小程序无需安装、门槛低、易分享,非常契合“到店扫码点餐”和“食堂临时用餐”的场景。小程序提供的wx.login+code2Session能力,可以快速获取用户唯一标识openid,后端无需自建用户账号体系,这对中小餐饮门店来说是巨大便利。
后端选择 Spring Boot 则是基于几个现实考量:生态成熟,几乎任何你需要的中间件都有现成 starter;和微信支付、对象存储、消息推送等服务的 SDK 集成资料丰富;打包部署(java -jar)简单,一台普通服务器即可。同时,Spring Boot 的自动装配和分层架构非常容易把“点餐系统”这种典型业务组织得干净整洁——Controller 管入参出参,Service 管业务逻辑,Mapper 管数据库访问。
整套系统我建议采用单体应用结构,不要在一开始就引入微服务和各种消息中间件。单体应用调试方便、部署简单、事务控制直接,完全足够支撑食堂或中小餐厅的并发量。等真的有性能瓶颈,再按模块拆分也不迟。
2. 系统架构与数据库建模
2.1 分层架构设计
实际项目中我采用了经典的五层结构:前端展示层(微信小程序)、网关/控制层(Controller)、业务逻辑层(Service)、数据访问层(Mapper/DAO)、基础设施层(MySQL + Redis + 对象存储)。
严格让 Service 层承载所有业务规则,Controller 只做参数接收和结果封装。有一个小技巧:所有接口统一返回Result<T>结构,包含code、message、data三个字段。这样小程序端处理响应时只需要写一个公共的请求封装方法,省去大量重复判断。
权限控制方面,小程序端请求需要校验登录态。实际上不需要依赖 Spring Security 那么重的框架,自定义一个拦截器,从请求头获取token,然后去 Redis 中查openid,查不到就返回401,让小程序重新登录。商家端后台则单独用一套 Session 管理管理员账号,避免和小程序用户混在一起。
2.2 核心数据表设计
这部分是很多人容易忽略但最重要的环节。表设计不合理,后面写功能会一直别扭。下面列出我最终采用的表结构,字段已经过精简,但保持了关键业务语义。
用户表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| openid | varchar | 微信小程序用户标识,唯一 |
| nickname | varchar | 用户昵称 |
| avatar | varchar | 头像路径 |
| phone | varchar(11) | 手机号(可选绑定) |
| create_time | datetime | 首次登录时间 |
菜品表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| category_id | bigint | 分类 ID |
| name | varchar | 菜品名称 |
| image | varchar | 图片路径 |
| price | decimal(10,2) | 原价 |
| stock | int | 当日库存,-1 表示不限量 |
| description | varchar | 文字描述 |
| status | tinyint | 1 上架 / 0 下架 |
| create_time | datetime | 创建时间 |
订单表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar | 业务订单号,唯一 |
| user_id | bigint | 用户 ID |
| shop_id | bigint | 店铺/食堂 ID |
| total_amount | decimal(10,2) | 商品总金额 |
| discount_amount | decimal(10,2) | 优惠金额 |
| pay_amount | decimal(10,2) | 实付金额 |
| status | tinyint | 0 待支付 / 1 已支付 / 2 已接单 / 3 制作中 / 4 已出餐 / 5 已完成 / 6 已取消 |
| remark | varchar | 用户备注 |
| create_time | datetime | 下单时间 |
订单明细表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_id | bigint | 订单主键 |
| dish_id | bigint | 菜品 ID |
| dish_name | varchar | 菜名快照,防止菜品修改后历史订单错乱 |
| dish_image | varchar | 图片快照 |
| price | decimal(10,2) | 下单时单价 |
| quantity | int | 份数 |
| subtotal | decimal(10,2) | 小计 |
美食活动表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar | 活动名称 |
| type | tinyint | 1 满减 / 2 折扣 / 3 直减 |
| start_time | datetime | 开始时间 |
| end_time | datetime | 结束时间 |
| threshold | decimal(10,2) | 门槛金额(满减活动用) |
| discount_amount | decimal(10,2) | 优惠金额(满减活动用) |
| discount_rate | decimal(3,2) | 折扣率(折扣活动用) |
| dish_ids | varchar | 参与菜品 ID 列表,逗号分隔;为空表示全场 |
| status | tinyint | 0 草稿 / 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 统一响应体与全局异常处理
所有接口返回统一Result<T>结构。我习惯在Result中加入静态工厂方法用于成功/失败返回:
public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(int code, String message) { Result<T> 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-size=5MB配置,并校验文件扩展名和 MIME 类型,才把这个问题按住。
5. 美食活动模块的算法设计与防坑
5.1 活动优惠计算的核心逻辑
我写了一个独立的ActivityCalculator组件,专门负责活动规则计算,避免散落在各个 Service 中。
public ActivityCalcResult calc(List<OrderItem> 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; List<String> 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.8+
- Maven 3.6+
- MySQL 5.7+ / MySQL 8.0+
- Redis 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 中增加characterEncoding=utf8参数,并配置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,那你收获的不仅是一份源码,更是一整套企业级业务系统的设计思维。