简介:在线订餐系统HTML5前端模板,面向Web前端开发者与全栈学习者,用于快速搭建具备现代交互体验的订餐平台。模板集成了餐厅菜单展示、订单提交、配送状态跟踪等核心模块,重点演示响应式布局、移动端适配、支付接口预留、后端API对接等关键设计,可作为课程设计、毕业项目或商业项目起步原型。资源包共194个文件,以120余张PNG切图、30张JPG素材为主,配套JavaScript交互脚本、GIF动效、HTML页面与CSS样式表,另含字体图标与地图数据文件,整体仅3.12MB,结构紧凑、便于部署。目前已有486人学习使用,适合需要参考完整前端实现的中级开发者。通过分析模板源码,可掌握Bootstrap栅格布局、Ajax异步交互、表单验证、性能优化及HTTPS安全传输等实战技能,同时了解SVG图标、Web字体加载与跨浏览器兼容处理,对理解现代前端工程化流程有直接帮助。 前阵子帮人做了一套在线订餐系统,从需求梳理到部署上线前后折腾了三周。这个项目看起来就是个常见的业务系统,但真正做进去才发现,坑基本都藏在订单、支付、库存这些核心链路里。这篇文章把这套系统的完整落地过程掰开揉碎讲一遍,包括技术选型、数据库设计、关键流程实现,还有那些测试环境根本暴露不出来的实际问题。不管你是准备拿它当毕业设计,还是想给自己的简历加一个完整项目,或者公司食堂真需要这么一套东西,都可以参考。
1. 项目整体定位与功能边界梳理
1.1 这套系统到底要解决什么问题
在线订餐系统本质上是一个连接用户、商家、骑手(或者自提柜)三方的交易平台。用户侧要能浏览菜单、加购、下单、支付、跟踪订单状态;商家侧要能管理菜品、上下架、接单、出餐;管理后台要能看到所有订单数据、处理退款、管理用户和商家账号。如果还涉及配送,就得有骑手端或者配送调度模块。
我这次做的是一个小型餐饮连锁品牌的在线订餐系统,服务范围覆盖三家门店。核心诉求很明确:顾客通过小程序在线下单支付,门店后厨自动接单出餐,自提和外卖两种履约方式。这个定位决定了系统不需要太复杂的营销功能,但订单状态流转和支付对账必须做得非常扎实。
1.2 角色划分与权限设计
系统涉及四种角色,每个角色的功能边界必须从一开始就划清楚。用户端包含注册登录、浏览菜品、下单支付、订单查询、售后申请;商家端包含菜品管理、订单接单/拒单/出餐、营业状态设置、数据统计;管理后台包含全平台订单监控、退款审核、用户/商家管理、基础数据配置;骑手/自提环节则通过订单状态同步来完成履约闭环。
权限设计上,我用了简单的RBAC模型,角色表、用户表、角色用户关联表,再加上一个权限点表。实际开发中不需要做到Spring Security那种粒度的细权限控制,但菜单级别的权限区分还是要有,否则商家端和管理后台混在一起,后面越做越乱。
2. 技术选型:为什么是这套组合不是别的
2.1 前端:小程序 + 管理端Web双端并行
用户端选了微信小程序,原因很直接:不需要下载App,微信生态内打开即用,支付直接走微信支付能力,对中小餐饮商家是门槛最低的方案。小程序端用原生语法开发,没有引入额外框架——对于这种页面数量在30个左右的项目,原生小程序完全够用,引入Taro或者uni-app反而增加一层编译成本和调试复杂度。
商家端和管理后台合并成了一个Web项目,用Vue3 + Element Plus实现。Vue3的组合式API在这种中后台场景下写起来很舒服,逻辑复用比Options API直接很多。Element Plus的表格、弹窗、表单组件基本覆盖了后台管理的全部需求,不需要自己造轮子。
2.2 后端:Spring Boot依然是中小型业务系统的最优选
后端选了Spring Boot 2.7 + MyBatis-Plus + MySQL + Redis的组合。为什么不用Spring Cloud那套微服务?原因很简单:业务复杂度还没到需要拆分的程度,三个门店的订餐量,单机部署的Spring Boot在性能和可靠性上都绰绰有余。引入微服务等于给自己增加服务发现、配置中心、分布式事务这些额外负担,对项目本身没有实际收益。
MyBatis-Plus比原生MyBatis舒服的地方在于单表CRUD不需要写XML,代码生成器可以直接生成实体类、Mapper、Service、Controller一层,省掉大量重复劳动。复杂查询仍然可以手写SQL控制,性能和灵活性都有保障。
Redis在这个项目里承担了三个职责:缓存菜品列表和门店信息、实现购物车(后面会详细说为什么用Redis而不用数据库)、处理分布式锁(主要是库存扣减和防止重复下单)。订单数据没有放在Redis里,因为订单需要严格持久化和事务保障,缓存订单反而增加一致性维护的复杂度。
2.3 为什么放弃了一些看起来更"高级"的方案
下单的时候很多人第一反应是上消息队列,比如RocketMQ或者RabbitMQ,觉得异步削峰才专业。但这个项目的峰值QPS撑死也就几十,数据库完全扛得住。引入MQ意味着还要部署Broker、处理消息可靠投递、解决消息乱序,这些复杂度在这个量级下都是负资产。
文件存储没有用FastDFS或者MinIO,就用了服务器本地目录加Nginx映射。菜品图片量不大,一台服务器硬盘绰绰有余,后续If需要迁移对象存储,只需要改一个文件上传的工具类就好。做技术选型最忌讳的就是"看着厉害就上",一切应以实际业务量级来定。
3. 数据库设计:一个在线订餐系统最核心的几张表
3.1 订单主表与订单明细表的设计要点
订单相关表是整个系统的心脏,设计得好不好直接决定后续开发的顺畅程度。订单主表我命名为orders(避免用order这个SQL关键字),核心字段如下:
CREATE TABLE `orders` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL COMMENT '下单用户ID', `store_id` bigint(20) NOT NULL COMMENT '门店ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `pay_amount` decimal(10,2) NOT NULL COMMENT '实付金额', `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT '优惠金额', `order_status` tinyint(4) NOT NULL COMMENT '订单状态:0待支付 1已支付 2制作中 3待取餐/配送中 4已完成 5已取消 6退款中 7已退款', `pay_type` tinyint(4) DEFAULT NULL COMMENT '支付方式:1微信支付', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `receiver_name` varchar(50) DEFAULT NULL COMMENT '收货人姓名', `receiver_phone` varchar(20) DEFAULT NULL COMMENT '收货人电话', `receiver_address` varchar(255) DEFAULT NULL COMMENT '收货地址', `remark` varchar(255) DEFAULT NULL COMMENT '订单备注', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_store_id` (`store_id`), KEY `idx_status` (`order_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';订单号不直接用自增ID,而是独立生成一个业务订单号,这个细节很重要。自增ID暴露给用户后会产生两个问题:一是用户可以猜测订单数量,有数据泄露风险;二是多笔订单号连号,后面做对账和外部分发的时候不够灵活。订单号生成规则我用的是时间戳+门店编号+随机数,例如20250608103045000102,在并发量不高的场景下足够保证唯一。
订单明细表record每一条代表订单中的一个菜品,包含菜品ID、菜品名称(下单时快照,防止菜品改名影响历史订单)、数量、单价、总价。菜品名和价格必须做快照,不然商家改了菜单历史订单数据就乱了,这是交易系统的基本素养。
3.2 购物车的Redis数据结构选择
购物车如果存数据库,每一次增删改都要落库,频繁的读写对数据库压力不小。用Redis合适得多,我把每个用户的购物车存成一个Hash,key是cart:userId,field是菜品ID,value是数量。
HSET cart:1001 15 2 HSET cart:1001 22 1 HGETALL cart:1001选择Hash结构而不是String存JSON,原因是Hash可以单独修改某一个菜品的数量,不需要整个购物车取出来反序列化再存回去。批量查询菜品价格和库存时,拿到field列表后一次性查数据库,性能也足够好。购物车加入Redis的另一个好处是天然支持过期时间,用户长时间不操作自动清空。
3.3 菜品表与库存字段设计
菜品表的核心字段包括菜品名称、分类ID、价格、图片、描述、月销量、状态(上架/下架)、库存。库存字段我用了int类型,每次下单扣减时使用乐观锁:
UPDATE dish SET stock = stock - #{quantity}, sold = sold + #{quantity} WHERE id = #{dishId} AND stock >= #{quantity}这里用stock >= quantity作为条件,配合受影响行数判断,天然防超卖。并发场景下单条UPDATE语句是原子的,MySQL的行锁保证不会有两个请求同时扣减同一份库存。
4. 从下单到配送:核心业务流程的完整串联
4.1 下单接口的完整处理链路
用户点击下单后,后端接口按下面的流程处理。第一步校验购物车非空,并锁定用户ID防止越权访问他人购物车。第二步遍历购物车items,查询菜品当前状态和库存,发现已下架或库存不足立即返回错误。第三步重新计算订单金额,实际项目中前端只做展示,金额一律以后端计算为准。第四步扣减库存,用3.3节那个乐观锁UPDATE,失败则说明库存被抢光了。第五步生成订单主表和明细表,先插主表拿到订单ID再批量插明细。第六步清空Redis购物车。第七步调用微信支付统一下单接口,拿到支付参数返回给前端。
这里有一个重要的细节:库存扣减和订单创建不是原子的。如果第五步失败,库存已经被扣了,需要补一个补偿机制。我在实践中用的方案是:扣库存和生成订单放在同一个事务里,保证要么都成功要么都失败。购物车清空在事务提交之后执行,因为即使购物车删除失败,用户重新进入购物车还能看到之前的商品,最坏情况是重复下单,不会出现订单有了但购物车没清的数据不一致。
4.2 支付回调处理:幂等性是最容易忽略的坑
微信支付的支付结果是以异步回调通知商户后端为准的。回调接口的处理逻辑直接决定资金安全和对账准确性。核心代码如下:
@PostMapping("/wxpay/notify") public String handleWxPayNotify(@RequestBody String xmlData) { // 1. 验签 WxPayNotifyResult result = wxPayService.parseOrderNotifyResult(xmlData); // 2. 根据商户订单号查询本地订单 Orders order = ordersMapper.selectByOrderNo(result.getOutTradeNo()); // 3. 幂等校验:如果订单已经是已支付状态,直接返回成功 if (order != null && order.getOrderStatus() == 1) { return "success"; } // 4. 校验金额是否一致(防篡改) if (order != null && order.getTotalAmount().equals(result.getTotalFee())) { // 5. 更新订单状态 ordersMapper.updateStatus(order.getId(), 1, new Date()); return "success"; } return "fail"; }为什么必须做幂等?因为微信支付的回调机制是:如果商户没有返回success,微信会按间隔递增的策略重试多次。如果回调处理中因为网络抖动重复执行,没有幂等保护就会出现订单状态反复更新的问题。第一次回调已经把订单改成已支付,第二次回调如果不判断直接再执行一次,虽然结果可能还是已支付,但相当于把用户已经完成的订单又重新"支付"一遍,一旦后续有短信通知、积分赠送这类扩展逻辑,就等着被骂吧。
4.3 订单状态机:用一张图想清楚所有流转路径
订单状态管理不能只靠if else硬写,一定要先画状态流转图再写代码。状态流转如下:待支付可以取消或支付成功进入已支付;已支付后商家接单进入制作中,也可以拒单进入退款中;制作中完成出餐进入待取餐/配送中;待取餐用户取走或骑手送达后进入已完成;已支付之后用户申请退款走退款中,退款审核通过进入已退款。
代码层面我封装了一个OrderStatusEnum枚举,每个状态定义可流转到的下一个状态集合,更新时校验流转合法性。这样做的好处是后续加新状态(比如"商家已接单"和"制作中"分开)时,只需要在枚举里加一个值并调整流转关系,不会出现改一处漏一处的情况。
5. 实际开发中踩过的坑与排查实录
5.1 并发场景下的库存超卖问题
第一次联调测试的时候,我用Jemeter模拟了100个并发请求同时购买最后一个库存的菜品,结果生成了11张订单。问题出在最开始的代码是先查询库存判断是否大于0,再执行UPDATE扣减。两个请求同时查到库存为1,都通过了判断,然后都执行了扣减,最终库存变成-1。后来改成UPDATE语句直接带stock >= quantity条件,利用数据库行锁保证原子性,同样场景再测只会有1个请求成功。这个问题是秒杀系统、限量商品系统的经典问题,但基础的业务系统同样会踩。
5.2 微信支付回调与本地订单状态不一致
上线第二天就遇到一个诡异问题:用户反馈支付成功了但订单还是待支付状态。排查日志发现微信在某个时间点同时发送了两次回调,第一次处理成功,第二次刚进入处理器时被Redis分布式锁拦截抛了异常,代码没有捕获这个异常导致微信认为回调失败又开始重试。后来在回调入口加了try-catch,所有异常都记录日志并返回fail,让微信继续重试,问题解决。还有一个容易遗漏的问题是回调接口超时时间,如果业务处理超过微信的限制,调用方会超时断开,所以回调里只做最核心的校验和状态更新,短信通知、消息推送这些全部异步处理。
5.3 数据库连接池配置不当导致的接口雪崩
压测的时候发现,用户下单接口的耗时从平均200ms直接飙升到5秒以上,数据库连接池报"Connection is not available, request timed out"。原因是HikariCP默认最大连接数是10,下单流程中长时间持有数据库连接(比如在事务里调用微信支付,微信接口响应2秒,这2秒连接一直被占用),并发一上来连接池就耗尽。解决方案是把最大连接池调到50,同时规范事务边界:事务里只做数据库操作,像调用微信支付、发送短信这类远程调用全部挪到事务外边,事务提交成功后再执行。这类问题在开发环境几乎不会暴露,只有压测或者真实高并发才会现出原形。
5.4 商家端"接单"重复提交
商家快速点击两次接单按钮,后端收到了两个请求,第一个把订单状态从已支付改成制作中,第二个进来发现订单已经是制作中,但代码里没有做状态校验,又执行了一次UPDATE,导致操作日志出现两条"接单"记录。解决方式是在状态更新SQL里带上当前期望的状态:
UPDATE orders SET order_status = 2 WHERE id = #{id} AND order_status = 1受影响行数为0说明状态已经被改过了,直接返回"订单状态已变更"。
6. 项目部署与上线后的持续优化
6.1 用Docker Compose一把梭部署
部署方案用的是Docker Compose,一条命令把MySQL、Redis、Spring Boot应用、Nginx全部拉起来。Nginx的配置里同时做了三件事:托管前端打包后的静态文件、反向代理/api路径到后端服务、处理图片静态资源的访问。HTTPS证书用泛域名证书直接配置在Nginx层,后端服务本身不需要处理SSL,简化了Spring Boot的配置。小程序端有域名白名单校验,上线前记得把HTTPS域名配到小程序后台,否则真机预览会报"域名不合法"。
数据库初始化用了一组SQL脚本,在docker-entrypoint-initdb.d目录下放schema.sql和data.sql,MySQL容器首次启动时自动执行。这个团队协作时特别省心,小伙伴clone代码后docker compose up -d就能拿到一套完整的本地环境,不用手动导入数据库。
6.2 上线后需要盯的三个核心指标
第一个是支付成功率和支付回调耗时。我在支付回调的入口加了一个计数器,每次回调打一条日志,用ELK或者简单的Shell Script定时扫日志就能看到平均值和异常值。如果回调耗时突增,优先检查数据库连接池和Redis响应。第二个是超时未支付订单的数量和转化率,这个数据直接影响商家的营收,一般通过定时任务每分钟扫一次将超过15分钟的待支付订单自动取消,参数做成可配置,方便根据不同门店调整。第三个是菜品售罄提示准确度,库存没有及时同步会导致用户下单成功但商家无货,我加了一个下单前再校验库存的兜底逻辑,一旦发现库存不足直接拒绝下单并给用户明确提示。
6.3 后续还可以继续扩展的方向
这个项目做完后,很多场景可以继续深化。比如加入优惠券系统,涉及券的发放、核销、与订单金额的联动;比如对接第三方的配送平台API,下单后自动呼叫骑手并把配送状态回传到订单;比如增加多门店的库存分仓管理,不同门店独立库存互不干扰;再比如把订单数据接入商业智能报表,做菜品销量分析、用户复购分析。这些都是在线订餐系统从"能用"到"好用"需要迈过的坎。
我个人实际操作中最深的体会是,这种业务系统写代码的时间大概只占四成,剩下的时间都在梳理流程和排查边界情况。把订单状态流转、支付回调、库存扣减这三块想透彻了,整个项目就稳了一大半。希望这套思路和踩坑经验能帮你少走一些弯路。
本文还有配套的精品资源,点击获取