☰
Spring Boot+微信小程序:咖啡店点餐系统全栈实战解析
2026/10/11 17:37:47 网站建设 项目流程

做咖啡店点餐系统这个选题,多半是因为它“麻雀虽小五脏俱全”。我刚接这个项目的时候,客户的要求很朴素:顾客用微信扫码进小程序自己点单,前台能接单,后厨能看到制作列表,账单别算错。真把整个链路拆开才发现,这个系统横跨小程序端、服务端接口、数据库设计、微信支付闭环、商家管理后台五个部分,随便哪一环粗糙处理都会让线上运营出岔子。这篇文章我会把这套系统的完整实现过程按真实项目落地顺序写出来,从技术选型、表结构、接口规划到核心业务逻辑和部署排查,给准备自己动手做的同学一个可以直接抄作业的参考。

1. 项目背景与整体设计思路

1.1 咖啡店点餐场景的痛点分析

咖啡店的点餐场景和正餐餐厅不太一样。正餐餐厅有服务员桌边点餐、传菜流程复杂,咖啡店的客单价相对集中、出品速度快、高峰期集中,而且现在很多店都是“小店模式”,一两百平米、两三个店员,如果每个顾客都需要吧台排队点单,高峰期肯定会积压。还有个痛点在于咖啡的“定制选项”比较多——甜度、温度、豆子类型、是否需要打包,这些如果由服务员手工录入或口头确认,很容易出错。

我们做这套基于微信小程序的点餐系统,核心目标就是让顾客自己把需求填清楚,订单直接推送到后厨屏或前台小票机,把人工确认环节压到最低。顾客扫桌上码或者到店扫码,就能看到分类菜单、选购下单、在线支付,之后凭取餐号去吧台取咖啡。整个流程对顾客来说是自助的,对商家来说是自动接单的,在人力有限的情况下可以明显提高高峰期的出单速度。

1.2 系统整体功能拆解

整个系统按角色可以分成三类使用对象:顾客(小程序端)、商家(管理后台)、系统管理员(后台账号管理)。顾客端的主要功能模块包括扫码进入门店、菜单展示与分类筛选、商品详情与定制选项、购物车、订单确认与支付、订单状态查看、历史订单与评价;商家端集中在管理网页里,包括商品管理和上下架、库存管理、订单接单与制作完成、营业报表、优惠策略配置;系统管理员则负责店员账号、门店信息、基础参数维护。

这个功能划分逻辑很简单:小程序端只做C端展示和交互,管理后台做B端运营,中间通过统一的后端接口层对接。模块拆得很清晰,后续再加功能也很方便。比如有的咖啡店后来又提出了“预约自取时间”的需求,就只需要在订单表加两个时间字段,小程序端下单页多两个选择器,后端在生成订单时多存两个字段,完全不影响其他模块。

1.3 前后端整体交互流程

整体的数据流大概是这样的:顾客打开小程序,前端通过微信登录拿到临时code,提交给后端,后端再去微信的接口换取该用户的openid,完成静默注册或登录;拿到用户身份之后,小程序端请求商品分类与商品列表,渲染菜单页面;顾客加购、选规格、提交订单,后端生成订单并调用微信支付统一下单接口,拿到支付参数后小程序端调起支付;支付完成后微信服务器异步回调后端通知支付结果,后端修改订单状态并推送“新订单提醒”给商家端;商家操作接单或制作完成,顾客在订单详情页看到状态变化,最后可以评价。

这里面有两个关键点值得注意。第一,微信支付必须用的是企业主体的小程序,个人主体做不了真实支付,这也是很多毕业设计里用“模拟支付”代替的原因;第二,支付结果以回调为准,不能只依赖前端返回的成功状态,否则金额对账一定会出问题。关于这两点后面的章节我再详细展开。

2. 技术选型与核心组件解析

2.1 后端主框架:Spring Boot 的选型理由

后端选Spring Boot,可以说是目前这类小程序管理系统最主流的选择。它的优势在于生态成熟、起步成本低,内嵌的Tomcat让部署只需要打一个jar包就能跑起来,不像以前用SSH框架要配置一堆XML。Spring Boot的自动配置大幅减少了样板代码,结合Spring MVC写RESTful接口非常顺手,再配合Spring Validation做参数校验、Spring Data Redis做缓存和Token存储,整个后端模块的搭建周期可以压缩到很短。

具体到我们这个咖啡店点餐系统,Spring Boot还有个隐性优势:社区资料特别多,遇到问题搜索解决方案的效率比其他技术栈高很多。比如MyBatis Plus的分页插件配置、Sa-Token或JWT的接入方式、支付回调验签的逻辑,网上都有大量现成案例可以对照。对于学生朋友或独立开发者来说,时间成本是最宝贵的,选Spring Boot基本不会踩“框架没人用、遇到问题找不到答案”的坑。

2.2 数据存储方案:MySQL 与 Redis 的分工

数据存储我用了MySQL加Redis的组合。MySQL负责所有持久化数据,包括用户表、商品表、订单表、订单明细表、分类表、门店配置表等。Redis则承担三块工作:一是保存微信小程序端的登录态或Token,避免每次请求都去数据库查用户;二是缓存商品分类和热门商品列表,小程序端的菜单页面打开频率很高,从缓存里拿可以明显降低数据库压力;三是处理少量热点数据的计数,比如今日订单量,虽然这个场景比较简单,但用Redis做一个原子自增会很方便。

MySQL这边表结构设计有一个经验一定要强调:金额字段一律用Decimal,不要用Float或Double。我见过好几个项目里咖啡价格用Double存,结果前端传 19.9 进来,数据库里变成 19.899999,订单合计出现莫名其妙的差一分钱问题。Decimal设置精度和标度为(10,2)就足够了,万一以后要做满减活动、优惠券分摊,两位小数也能保证精度不出错。

2.3 小程序端技术方案

小程序端我选择的是原生微信小程序开发而非uni-app。为什么?虽然uni-app可以一套代码多端复用,但对于这个项目,目标平台很明确就是微信生态,原生框架在权限管理、微信登录、支付调起、订阅消息这些能力上都更直接,调试也更方便。原生小程序有自己的组件生命周期和页面路由机制,熟练之后开发速度其实不比uni-app慢。

如果后续确实有上线App的需求,再把业务逻辑抽到uni-app重写也不迟。在项目开发阶段,保持简单直接很重要,非必要不引入跨端框架。这个项目里小程序端的核心页面包括首页/菜单页、商品详情页、购物车页、订单确认页、订单列表页、订单详情页、个人中心页,总共七个页面,原生开发足够覆盖。

2.4 商家管理后台方案

商家管理后台我用了Vue加Element UI,单独做一个Web项目。为什么不直接用小程序管理?小程序做复杂表格和图表太别扭了,商品管理、订单列表这种操作密集型的场景,Web的效率和体验完胜。管理后台与后端通过同一套接口交互,只是登录认证方式不同:小程序端用微信登录换Token,管理后台用账号密码登录拿Token。

管理后台的模块和C端逻辑对应,包括仪表盘(今日订单数、营收、热门商品排行)、商品管理(分类维护、商品增删改、上下架)、订单管理(列表筛选、接单、制作完成、出餐)、以及门店基础信息的配置。对于小店需求来说,这套后台已经够用,不用再单独部署一套复杂的BI系统。

3. 数据库设计与表结构详解

3.1 核心表清单概览

一张图说清楚这个系统的核心表设计。总共八张业务表:user(用户)、category(分类)、product(商品)、product_spec(商品规格,用于甜度、温度等选项)、cart(购物车)、orders(订单主表)、order_item(订单明细)、store_config(门店配置)。

user:id, openid, nickname, avatar, phone, balance, create_time category:id, store_id, name, sort, status product:id, category_id, name, image, desc, price, status, stock, sales, create_time product_spec:id, product_id, spec_name, spec_value, extra_price cart:id, user_id, product_id, product_spec, quantity, checked, create_time orders:id, order_no, user_id, total_amount, pay_amount, pay_type, status, pickup_code, remark, create_time, pay_time order_item:id, order_id, product_id, product_name, product_spec, price, quantity, subtotal store_config:id, store_name, address, phone, business_hours, notice

我刻意没有加冗余的字段,比如订单表里不存门店名称,因为单店模型里一个门店的配置是全局的,查询的时候联表或者直接查配置就好,没必要在每张订单上重复存一份。但如果你们做的是多门店连锁模型,那订单表就必须加store_id了,设计时要想清楚边界。

3.2 订单表设计的关键考虑

订单表是整个系统的核心,设计上有一个地方必须事先想明白:订单状态怎么流转。我定义的状态集合是这样:0待支付、1已支付待取餐、2制作中、3已完成、4已取消、5退款中、6已退款。这里有一个细节,制作中状态是否需要独立,取决于商家有没有后厨屏的“接单”动作。如果商家希望在接单后再开始制作,那“待接单”和“制作中”就要分开,如果店里流程简单,可以直接支付后就进入待取餐,这个可以灵活处理。

订单号字段也值得说一下。我没有用自增ID直接当订单号暴露给用户,而是单独生成一个业务订单号,格式是时间戳加四位随机数,比如咖啡店用量不大,这个长度完全够用。自增主键可以作为内部关联用,但对外展示和支付回调关联都用业务订单号,避免用户看到不连续的订单编号猜测订单量。

3.3 库存与销量的并发处理

库存的设计,立flag的场面又来了。咖啡店的点餐场景库存概念和电商不太一样:电商是下单减库存,到支付超时再回补库存;咖啡店则是支付后减库存,因为出杯量有限,支付成功才真正占用了制作资源。所以在Hot咖啡店的系统里,订单状态从0待支付变成1已支付的时候,才去执行库存扣减和销量增加。如果用户支付之后只要还在待取餐状态,库存就已经扣了。

扣减库存时我用了一条带乐观锁语义的更新SQL,防止两个人同时买了同一杯“今日特供”,结果库存只减了一次:

UPDATE product SET stock = stock - #{count}, sales = sales + #{count} WHERE id = #{productId} AND stock >= #{count}

这条SQL执行后如果影响行数为0,说明库存不足,下单接口直接抛出业务异常,让前端提示“商品已售罄”。这个方案比先查再改再加分布式锁要简单可靠得多,MySQL行锁天然帮我们处理了并发问题。

4. 小程序端核心功能实现要点

4.1 微信登录与Token管理

小程序的登录流程是每个做过微信项目的人都绕不开的环节。微信官方推荐的流程是:wx.login获取临时code,传给后端/api/auth/login接口,后端拿这个code加上小程序的AppId和AppSecret请求微信接口,换取openid和session_key。拿到openid后查用户表,如果不存在就自动创建一条新用户,存在则直接返回登录成功。

登录成功后的Token方案,我选择了比较轻量的Sa-Token或自定义JWT,两者都可以。用JWT的话注意一下,客户端请求时在header里带Authorization字段,后端用一个拦截器统一校验Token,校验通过后从Token里解析出userId放到请求上下文。注销、Token过期这些方法JWT做起来稍麻烦,但对于课程设计或个人项目,JWT的简单性足够。这里有个小坑:小程序前端每次请求都要手动在header里带Token,可以用wx.request封装一个统一请求方法,内部读取本地存储的Token并自动加在请求头里,省得每个页面重复写。

4.2 菜单展示、购物车和下单

菜单页面是这个系统首页即核心的页面。小程序端用scroll-view配合分类导航实现左侧分类、右侧商品列表的布局,右侧商品列表用纵向列表展示商品图和价格,点击某一项弹出商品详情或规格选择底部弹窗。规格选择这个功能对咖啡店尤其重要——冰量、甜度、浓度,每个商品可以有多个规格维度,每个维度里可以有不同的选项。

购物车这里有一个交互细节:小程序里常驻一个底部购物车横条,加购后显示总价和购物车数量,点击横条弹出购物车商品列表,可以直接修改数量或清空。这个交互非常好用,比跳转一个独立购物车页的路径更平滑。我在手机上实测,顾客点击“去结算”时潜意识里对已选商品的确认成本更低。

4.3 支付流程与回调处理

这块是整套系统里最容易出问题的地方。在实际支付流程中,wx.requestPayment需要拿到后端预支付交易单的参数。后端在生成订单后调用微信支付的统一下单接口,拿到prepay_id,再按规则生成小程序端所需的paySign。前端调起支付后会在success回调里拿到一个结果,但这个结果只表示微信支付收银台被成功调起,并不能代表支付真的成功了。真正的支付结果,是通过微信服务器异步回调后端接口的方式通知的。

所以后端的设计必须是:统一下单时传入的notify_url指向我们自己的接口,支付成功后微信回调该接口,更新订单状态。不能信任前端回调里的状态,一切以异步通知为准。同时回调接口要做好验签,还有一个小技巧:回调可能因为网络问题重复发送多次,处理时要做幂等判断——如果订单已经是支付成功状态,就不再重复处理。

这里我不建议在这个系统里接完整的退款和退款回调,除非客户真的有这个需求。退款处理涉及原路退款、回调处理、订单状态回滚多个环节,复杂度会明显上升。如果只是课程设计或演示,在管理后台做一个可以标注“退款”状态的字段就够了。

4.4 订单状态的消息触达

咖啡店场景里,顾客最关心的是“我的咖啡好了没”。我实现的方式是:商家在管理后台点击“出餐完成”时,小程序端订单详情页通过轮询或WebSocket感知到状态变化。轮询实现最简单,全局每5秒请求一次订单详情,状态变了就刷新界面;WebSocket体验更好,但小程序端对WebSocket的封装和后台推送服务的搭建成本稍高。

刚开始做的时候我图省事,直接用轮询,后来发现流量消耗大,而且低并发下还能接受,高并发时后端压力上升明显。折中的方案是:仅在用户的订单详情页打开时轮询,离开页面就停止,再叠加一个微信订阅消息的推送提醒。第一次版上线后实测,如果做真实运营,建议用订阅消息推送“取餐提醒”给用户,体验会专业很多。

5. 后端接口设计与业务实现

5.1 接口风格与统一返回结构

后端接口我统一使用了RESTful风格,所有接口的返回结构也统一定义为一个Result类,包含code、message和data三个字段。code为0表示成功,非0为各类错误码。前端根据code判断接口调用是否成功,而不是依赖HTTP状态码——因为很多业务异常我们主动捕获后仍然返回200,这样小程序端的wx.request不会走fail逻辑,处理起来统一。

大概列出核心接口清单:

模块接口路径方法说明
登录/api/auth/loginPOST用code换取登录态
商品/api/product/listGET获取分类和商品
商品/api/product/detailGET商品详情含规格
购物车/api/cart/addPOST加入购物车
购物车/api/cart/listGET购物车列表
订单/api/order/createPOST创建订单
订单/api/order/payPOST调起支付参数
订单/api/order/detailGET订单详情
订单/api/order/listGET订单列表
支付回调/api/pay/notifyPOST微信异步回调

5.2 创建订单与调起支付的完整流程

创建订单接口是整个系统业务逻辑最集中的地方。调用/api/order/create时,后端需要做的操作包括:校验用户Token、从请求中获取购物车商品列表、遍历商品核算总金额、检查商品上下架状态和库存、生成订单主表和订单明细、清空购物车对应商品。这些操作必须放在一个事务里,任何一个环节出错都要整体回滚。这里要注意的是,校验库存时不要直接扣库存,保持0待支付状态时不占用库存,等到支付回调成功时再扣。

支付接口的参数构造细节是:后端要把订单金额、订单号、用户openid组装成微信支付要求的请求体,调微信的/v3/pay/transactions/jsapi接口(现在的V3版本),拿到prepay_id后,再用商户私钥对小程序Id + 时间戳 + 随机串 + prepay_id做签名。签名算法有点绕,直接用现成的工具类会省很多事。如果支付成功后要做积分或会员等级,也可以在这个回调方法里一起处理。

5.3 商家后台接口规划

商家后台的接口和管理端的前端页面一一对应。商品新增和编辑、分类维护、订单接单和出餐、取消订单、每日营业数据的聚合查询。营业数据查询这里我写了一条基于日期分组的SQL:

SELECT DATE(create_time) as day, COUNT(*) as order_count, SUM(pay_amount) as total_amount FROM orders WHERE status IN (1,2,3) AND create_time >= #{startTime} GROUP BY DATE(create_time) ORDER BY day DESC

注意这里统计的是支付成功的订单,不要把头取消的订单也算进营业额。对于偶尔出现的退款,则单独在退款表中记录下来,不给营业数据造成混淆。后台页面用Vue写,配合一个简单的表格和筛选条件,五分钟就能完成这块开发。

6. 核心业务场景的难点攻破

6.1 购物车的合并与批量操作

购物车看似简单,但细节很多。这个系统的购物车按用户维度存储,同一个用户加购同一个商品且规格完全相同,应该合并数量,而不是插入两条;如果规格不同,则分开记录。前端展示时按加入时间倒序排列。批量清除已下单的购物车项,一定要用delete from cart where id in (...)配合已选中的ID集合,而不是前端每删除一项就发一个请求,否则网络开销极大。

还有一点容易被忽略:商品可能在下单前被后台下架或价格调整。这时候创建订单接口需要重新从数据库取商品价格,而不是信任前端传过来的单价。前端展示的价格只是参考,以数据库实时价格为准,避免出现顾客看到的价格和结算价格不一致的纠纷。

6.2 金额计算与满减优惠

咖啡店经常做的活动是“满30减5”或“第二杯半价”。在订单创建服务中,金额计算我建议单独封装一个PriceCalculator类,不要和其他业务逻辑混在一起。这个类输入购物车商品列表和活动规则,输出原始金额、优惠金额、实付金额。活动规则尽量从数据库读取,而不是在代码里写死——因为一周后店长可能又改了活动方式。

金额计算时还有一处细节:所有中间计算结果都不要四舍五入,最后实付金额再统一保留两位小数。比如“第二杯半价”如果直接按单杯价格算,17块钱一杯第二杯半价就是8.5,但两杯一起按总价打折,可能算出8.4999这种数,如果每杯分别计算再相加,就保留两位小数就没有这个问题。这类边界情况在测试时一定要覆盖。

6.3 超卖与重复支付的防护

前面说到的UPDATE库存带条件判断就是防超卖的关键。重复支付的防护则要保证幂等性:微信异步回调可能因为网络原因重试多次,回调方法里第一步就是查订单,如果状态已经是1已支付,直接返回成功通知,不再重复处理。同时在改订单状态时用UPDATE orders SET status = 1 WHERE id = ? AND status = 0,利用行锁确保状态只能从待支付改成已支付一次。

数据库层面可以给订单状态加索引和约束,应用层面加幂等判断,双层保障。虽然支付重复回调的概率本身很低,但真遇到一次就会导致订单状态错乱和库存多扣的严重问题。当时我在本地模拟重复回调时,就是因为没有判断状态导致扣了两次库存,这种坑写出来提醒大家一定要先判断再更新。

6.4 并发领取优惠券与超发问题

如果系统里设计了优惠券模块,还有一个并发问题要处理。在咖啡店场景里,常有“到店扫码领券”的活动,多个顾客同时扫同一个码,可能出现券被超发的情况。处理方式可以在券模板表里加一个remaining字段,领取时用条件更新扣减。和库存扣减是同一种思路。

7. 环境配置与部署上线的常见问题

7.1 小程序合法域名与HTTPS配置

后端接口做上线部署时,第一个拦路虎就是小程序的合法域名配置。微信规定,wx.request的域名必须在微信公众平台后台配置为HTTPS的合法域名,开发阶段可以在开发者工具里勾选“不校验合法域名”,但真机预览或线上体验时必须配置。所以要提前准备好备案过的域名和HTTPS证书,用Nginx把后端服务的端口代理到443上。

Nginx配置的主要作用是反向代理和HTTPS终结。将前端的请求通过域名转发到本机8080端口,让客户端只和域名通信。这里有几个细节:配置SSL证书时证书要放在服务器上,同时注意Nginx的keepalive参数,因为小程序的并发请求会复用连接,如果没有设置keepalive 128这类参数,连接反复建立会拖慢响应速度。

7.2 数据库的时区与连接池配置

数据库相关的坑比较隐蔽。第一个是时区问题,create_time如果使用MySQL的datetime类型,连接串里没有加serverTimezone=Asia/Shanghai时,从Java取出来的时间会差8个小时;第二个是连接池的配置,默认的初始连接数太小,高峰期一开小程序店面,连接池可能排队。这两个问题我在部署阶段都遇到过,定位起来不困难,但如果没有经验,很容易以为是代码逻辑写错了。

个人建议在application.yml里显式配置Druid或HikariCP的连接池参数,初始连接数设定为10,最小空闲连接数5,最大连接数50。另一个小技巧是,订单表的create_time加一个默认值CURRENT_TIMESTAMP,这样插入数据时不需要显式传时间,减少代码层面的遗忘。

7.3 真机调试和模拟器的差异

小程序开发里,模拟器表现和真机表现经常有差异。最常见的是底部安全区域适配问题——iPhone全面屏底部有一个横条区域,如果购物车横条设计得贴底,在真机上可能会跟Home指示条重叠。解决办法是给底部栏加上padding-bottom: env(safe-area-inset-bottom)。

真机的网络请求也有差异,模拟器里可以用局域网IP直连本机后端,但真机必须使用HTTPS域名。所以开发前就要准备好一个测试域名和测试证书,哪怕是自签名的证书,也要在小程序后台配置好,或者用微信开发者工具的“真机调试”功能,它会自动把请求代理到本地,这个方式能省很多事。

7.4 版本迭代时的数据库变更

开发到后期,加字段是家常便饭。我建议从一开始就给项目引入一个简单的数据库版本管理工具,比如Flyway,每次表结构变更写一个版本的迁移SQL。这样多人协作或换机器部署时只需要执行一次mvn flyway:migrate,数据库结构就更新了。如果不做版本管理,等到上线后给客户增改字段,你要手工跑到服务器执行SQL,一旦忘记执行某个脚本,线上就会出现诡异的接口报错。

我有一次就是因为商品表加了is_recommend字段后,本地数据库手动执行了SQL没有问题,但测试环境忘了执行,页面一度报“未知列”错误。排查了半天才发现是环境数据库不一致,引入Flyway之后再也没有出现这类问题。

8. 踩坑复盘与个人经验总结

这个项目做到最后,最深的体会是:一套点餐系统真正的工作量不在增删改查,而在于把业务边界想清楚。库存什么时候扣、支付状态信谁、优惠怎么算、并发怎么防,这四个问题想清楚了,后面就是体力活。很多同学拿到这个题目上来就写代码,做到中间才会发现订单状态流转和支付回调是最难改的部分,返工成本极高。

有几条经验可以说一下,对后续有类似项目一定有参考价值。

第一,先做核心理清状态机。订单状态的定义和流转图要画清楚,再动手建表。状态枚举字段用数字不要用字符串,数字保持稳定方便扩展;如果需要中文展示,前端自己去映射,不要在后端存中文。

第二,权限控制不能偷懒。小程序端Token管理要上线拦截器统一处理,管理后台的接口也不能裸奔。至少做角色区分,普通店员只能操作订单和商品,不能修改门店配置,管理员才有全部权限。哪怕是一个小店系统,权限控制也决定了后台能不能放心给员工用。

第三,测试案例要覆盖极端情况。购物车同时加两个相同商品时数量是否正确合并、支付并发回调是否重复处理、库存只剩1杯时两个人同时下单是否只有一个成功、订单金额计算0元或负数时是否抛异常。这些用例在写代码时就要在脑子里过一遍,把防御性校验写在接口的最前面,不要等用户操作出了问题再去修。

最后再分享一个小技巧。开发过程中小程序端的“体验版”二维码是一个很好用的协作方式,让客户或者店主扫一下二维码,就能在真机微信里直接使用系统。体验版在提交审核前可以随时更新,非常适合快速收集反馈、调整细节。我每次改动完关键流程,都会发一个新的体验版给客户测,几轮下来客户的实际需求就能摸得比较清楚。

这个系统做完之后,我又在它的基础上给另外两家店做过定制:一家加了预约自取功能,另一家做了针对会员储值的余额支付。核心结构没有动,只是在订单和支付模块上做扩展。这也说明设计良好的点餐系统本身就是一套可以复用的基础体系,从一个单店模型出发,往多门店、会员营销、后厨数字化管理方向延伸都比较顺畅。但第一版不要贪多,先把点餐、支付、出餐这条主链路跑通,比什么都重要。项目做到这里,我的总结是:复杂的事情简单做,简单的流程重复做,系统稳定上线的底气,就来自对每个业务细节的死磕。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询