一、项目背景:餐饮行业的点菜痛点与数字化需求
在传统灌汤小笼包饭店的运营中,点菜环节常面临三大核心痛点:
- 效率低下:服务员手写菜单、人工传菜,高峰期易出现漏单、错单,客户等待时间长;
- 信息滞后:菜品库存实时更新不及时,常出现 “客户点餐后才告知售罄” 的尴尬场景;
- 管理混乱:人工统计订单数据繁琐,无法快速分析热门菜品、客户消费习惯,影响运营决策。
为解决这些问题,本项目开发 “基于微信小程序的灌汤小笼包饭店点菜系统”,整合 “菜品展示、在线点菜、订单管理、库存同步、数据统计” 全流程功能。客户通过微信小程序即可完成点菜下单,商家通过后台实时处理订单,实现点菜环节的数字化、自动化,提升客户体验与运营效率。
二、核心技术栈:轻量高效的技术选型
系统围绕 “开发成本低、部署便捷、用户门槛低” 原则选型,技术栈覆盖小程序端、后端服务、数据存储,适配中小型餐饮门店的业务需求:
| 技术类别 | 具体选型 | 核心优势 |
|---|---|---|
| 小程序端 | 微信小程序原生开发(WXML + WXSS + JS) | 无需下载安装,客户通过微信扫码即可使用,降低用户门槛;支持微信支付、地理位置等原生能力,适配餐饮场景的支付、门店定位需求;界面轻量化,加载速度快,高峰期操作流畅。 |
| 后端框架 | SSM(Spring + SpringMVC + MyBatis) | Spring 实现事务管理(如 “下单后自动扣减库存”),确保数据一致性;SpringMVC 负责请求分发,适配小程序端与管理端的接口交互;MyBatis 支持自定义 SQL,灵活实现 “菜品筛选”“订单统计” 等复杂查询。 |
| 开发语言 | Java 11 | 稳定兼容 SSM 框架,支持面向对象编程,代码可复用性高;具备成熟的异常处理机制,便于排查线上问题。 |
| 数据库 | MySQL 8.0 | 开源免费,支持事务与索引(确保 “订单创建 - 库存更新” 原子性);支持百万级数据存储,满足门店每日订单、菜品数据的存储需求;适配 Windows/Linux 系统,部署灵活。 |
| 管理端前端 | JSP + Bootstrap + jQuery | Bootstrap 提供响应式布局,适配电脑 / 平板的管理端操作;jQuery 简化 DOM 操作与 AJAX 异步请求(如实时刷新订单状态);JSP 支持动态页面渲染,便于展示订单、库存等实时数据。 |
| 开发工具 | 微信开发者工具 + IntelliJ IDEA | 微信开发者工具支持小程序实时预览、调试;IDEA 支持 SSM 代码提示、断点调试,集成 Maven 管理依赖,提升后端开发效率。 |
| 辅助技术 | Redis + EasyExcel | Redis 缓存热门菜品数据(如 “销量 TOP10 小笼包”),减少数据库访问压力,提升小程序端加载速度;EasyExcel 实现订单数据导出(如 “每日订单明细 Excel”),满足商家统计需求。 |
三、系统分析:可行性与核心需求拆解
3.1 可行性分析
在系统开发前,从技术、经济、操作三个维度验证项目落地能力,确保系统可开发、低成本、易推广:
3.1.1 技术可行性
- 技术成熟度:微信小程序、SSM 框架均有完善的官方文档与社区支持(如微信小程序开发指南、MyBatis 中文文档),开发中遇到的问题可快速找到解决方案;
- 硬件要求低:开发阶段仅需普通 PC(i5 处理器 + 8GB 内存),生产环境可部署在云服务器(2 核 4GB 配置即可支撑 100 家门店同时在线);
- 场景适配性:核心功能(在线点菜、订单管理)均基于成熟技术方案,无复杂技术难点(如点菜流程基于 “小程序提交→后端处理→数据库存储” 的经典流程)。
3.1.2 经济可行性
- 开发成本低:所有核心技术均为开源免费(Java、SSM、MySQL、微信小程序),无软件授权费用;开发周期约 2-3 个月,2 人团队即可完成核心功能开发;
- 部署成本低:云服务器(阿里云 ECS 入门版)月均成本<100 元,搭配 MySQL 数据库(云数据库 RDS 入门版)月均成本<50 元,中小门店可轻松承担;
- 收益潜力大:系统可减少 30% 的服务员人力成本,同时通过 “热门菜品推荐” 提升客单价,短期即可覆盖开发与部署成本。
3.1.3 操作可行性
- 客户端门槛低:客户无需注册,通过微信扫码进入小程序,3 步即可完成点菜(选菜品→确认订单→支付),老人、小孩均可快速上手;
- 管理端易操作:商家后台界面参考传统餐饮管理系统,功能分区清晰(菜品管理、订单处理、数据统计),店员仅需 1 小时培训即可独立操作;
- 维护成本低:系统模块化设计,后期新增菜品、调整价格仅需在后台修改,无需改动代码;小程序端自动更新,无需客户手动升级。
3.2 核心业务流程分析
3.2.1 客户点菜流程
- 进入小程序:客户扫描门店桌贴二维码,进入灌汤小笼包饭店点菜小程序(自动定位当前桌号);
- 浏览菜品:小程序展示菜品分类(如 “招牌小笼包”“凉菜”“汤品”),客户可查看菜品图片、价格、库存,点击 “加入购物车”;
- 确认订单:客户进入购物车,调整菜品数量(如 “小笼包 2 笼”“酸辣汤 1 碗”),点击 “提交订单”;
- 在线支付:支持微信支付,支付完成后小程序生成订单号,同时向商家后台推送订单通知;
- 订单跟踪:客户可在 “我的订单” 中查看订单状态(“待接单”“制作中”“已完成”),避免反复催促服务员。
3.2.2 商家订单处理流程
- 接收订单:商家后台实时弹出新订单提醒(声音 + 弹窗),显示桌号、菜品、数量、支付状态;
- 确认接单:店员点击 “确认接单”,系统自动扣减对应菜品库存(如 “小笼包库存 - 2”);
- 制作菜品:后厨通过打印机自动打印订单小票,按小票制作菜品;
- 完成送餐:菜品制作完成后,店员点击 “已送餐”,小程序向客户推送 “菜品已送达” 通知;
- 订单归档:客户用餐结束后,系统自动将订单标记为 “已完成”,支持导出 Excel 用于财务统计。
四、系统设计:功能模块与数据库详解
4.1 系统功能结构设计
系统分为微信小程序端(客户使用) 与Web 管理端(商家使用) 两大模块,功能边界清晰,联动顺畅:
4.1.1 微信小程序端(客户功能)
| 功能子模块 | 核心操作 |
|---|---|
| 菜品浏览 | 按分类查看菜品(招牌小笼包、凉菜、汤品);查看菜品详情(图片、价格、库存、客户评价);搜索菜品名称快速定位。 |
| 购物车管理 | 加入菜品至购物车;调整菜品数量(增加 / 减少);删除购物车中不需要的菜品;查看购物车总价。 |
| 订单管理 | 提交订单(关联桌号);在线支付(微信支付);查看订单状态(待接单 / 制作中 / 已完成);查看历史订单。 |
| 门店定位 | 自动获取当前门店位置(支持多门店部署);显示门店营业时间、联系电话;提供到店导航(跳转微信地图)。 |
| 个人中心 | 查看个人消费记录;管理收货地址(用于外卖场景,可扩展);反馈意见(如 “菜品口味建议”)。 |
4.1.2 Web 管理端(商家功能)
| 功能子模块 | 核心操作 |
|---|---|
| 菜品管理 | 新增菜品(上传图片、填写名称 / 价格 / 库存 / 分类);编辑菜品信息(调整价格、更新库存);上架 / 下架菜品(下架后小程序端隐藏)。 |
| 订单管理 | 查看实时订单(按状态筛选:待接单 / 已接单 / 已完成 / 已取消);确认接单 / 标记送餐 / 取消订单;处理退款申请(如客户误下单)。 |
| 库存管理 | 查看所有菜品库存;批量调整库存(如 “补充小笼包库存至 50 笼”);设置库存预警(低于 10 份时提醒补货)。 |
| 数据统计 | 统计每日 / 每月订单量、营业额;分析热门菜品(按销量排序);统计客户人均消费;导出数据报表(Excel 格式)。 |
| 门店管理 | 新增门店(填写名称、地址、营业时间);编辑门店信息;管理门店桌号(新增 / 删除桌号,关联二维码)。 |
| 用户管理 | 新增店员账号(分配角色:管理员 / 收银员 / 后厨);设置账号权限(如 “收银员仅能处理订单,无法修改菜品”);禁用 / 启用账号。 |
4.2 数据库设计
数据库设计围绕 “菜品 - 订单 - 客户” 三大核心实体,设计 8 张关键数据表,确保数据关联清晰、冗余低、查询高效。
4.2.1 核心实体 E-R 图(关键实体)
- 菜品实体:包含菜品 ID、名称、图片、分类、价格、库存、销量、是否上架等属性;
- 订单实体:包含订单 ID、订单号、客户 ID、桌号、订单金额、支付状态、订单状态、创建时间等属性;
- 订单项实体:包含订单项 ID、订单 ID、菜品 ID、购买数量、菜品单价、小计金额等属性;
- 客户实体:包含客户 ID、微信 openid、昵称、头像、联系方式等属性(通过微信授权获取,无需客户手动填写);
- 门店实体:包含门店 ID、名称、地址、营业时间、联系电话、门店图片等属性。
4.2.2 核心数据表结构设计
以下为系统关键数据表的详细结构(字段名、类型、说明、是否为空):
表 4-1 菜品表(caipin)
| 序号 | 列名 | 数据类型 | 说明 | 允许空 |
|---|---|---|---|---|
| 1 | Id | Int(11) | 主键 ID | 否 |
| 2 | caipin_name | Varchar(100) | 菜品名称(如 “鲜肉灌汤小笼包”) | 是 |
| 3 | caipin_photo | Varchar(255) | 菜品图片 URL | 是 |
| 4 | caipin_types | Int(11) | 菜品分类(1 = 小笼包 / 2 = 凉菜 / 3 = 汤品) | 是 |
| 5 | caipin_kucun_number | Int(11) | 菜品库存数量 | 是 |
| 6 | caipin_old_money | Decimal(10,2) | 菜品原价(单位:元,用于折扣展示) | 是 |
| 7 | caipin_new_money | Decimal(10,2) | 菜品现价(单位:元) | 是 |
| 8 | caipin_sales | Int(11) | 菜品销量(用于热门排序) | 是 |
| 9 | shangxia_types | Int(11) | 是否上架(0 = 下架 / 1 = 上架) | 是 |
| 10 | caipin_content | Text | 菜品描述(如 “皮薄馅大,汤汁浓郁”) | 是 |
| 11 | create_time | Timestamp | 创建时间 | 是 |
表 4-2 订单表(caipin_order)
| 序号 | 列名 | 数据类型 | 说明 | 允许空 |
|---|---|---|---|---|
| 1 | Id | Int(11) | 主键 ID | 否 |
| 2 | order_uuid_number | Varchar(50) | 订单号(如 “OD20251115001”) | 是 |
| 3 | weixin_openid | Varchar(100) | 客户微信 openid(关联客户信息) | 是 |
| 4 | zhuohao | Varchar(20) | 桌号(如 “1 号桌”“包间 A”) | 是 |
| 5 | total_money | Decimal(10,2) | 订单总金额(单位:元) | 是 |
| 6 | pay_status | Int(11) | 支付状态(0 = 未支付 / 1 = 已支付) | 是 |
| 7 | order_status | Int(11) | 订单状态(0 = 待接单 / 1 = 已接单 / 2 = 制作中 / 3 = 已完成 / 4 = 已取消) | 是 |
| 8 | pay_time | Timestamp | 支付时间 | 是 |
| 9 | create_time | Timestamp | 订单创建时间 | 是 |
表 4-3 订单项表(order_item)
| 序号 | 列名 | 数据类型 | 说明 | 允许空 |
|---|---|---|---|---|
| 1 | Id | Int(11) | 主键 ID | 否 |
| 2 | order_id | Int(11) | 订单 ID(关联订单表主键) | 是 |
| 3 | caipin_id | Int(11) | 菜品 ID(关联菜品表主键) | 是 |
| 4 | buy_number | Int(11) | 购买数量(如 “2” 代表 2 笼) | 是 |
| 5 | caipin_price | Decimal(10,2) | 购买时的菜品单价(单位:元) | 是 |
| 6 | subtotal_money | Decimal(10,2) | 订单项小计金额(单价 × 数量) | 是 |
| 7 | create_time | Timestamp | 创建时间 | 是 |
表 4-4 客户表(customer)
| 序号 | 列名 | 数据类型 | 说明 | 允许空 |
|---|---|---|---|---|
| 1 | Id | Int(11) | 主键 ID | 否 |
| 2 | weixin_openid | Varchar(100) | 微信 openid(唯一标识) | 是 |
| 3 | nickname | Varchar(50) | 客户微信昵称 | 是 |
| 4 | avatar_url | Varchar(255) | 客户微信头像 URL | 是 |
| 5 | phone | Varchar(20) | 客户手机号(可选填写) | 是 |
| 6 | create_time | Timestamp | 首次使用时间 | 是 |
表 4-5 门店表(store)
| 序号 | 列名 | 数据类型 | 说明 | 允许空 |
|---|---|---|---|---|
| 1 | Id | Int(11) | 主键 ID | 否 |
| 2 | store_name | Varchar(100) | 门店名称(如 “XX 灌汤小笼包总店”) | 是 |
| 3 | store_address | Varchar(255) | 门店地址 | 是 |
| 4 | business_hours | Varchar(100) | 营业时间(如 “06:30-21:00”) | 是 |
| 5 | phone | Varchar(20) | 门店联系电话 | 是 |
| 6 | store_photo | Varchar(255) | 门店图片 URL | 是 |
| 7 | create_time | Timestamp | 创建时间 | 是 |
五、系统实现:核心功能代码与界面展示
5.1 微信小程序端核心功能实现(以 “菜品浏览与下单” 为例)
5.1.1 菜品列表页面(pages/caipin/list.wxml)
<!-- 菜品分类导航 -->
<view class="category-container">
<scroll-view scroll-x class="category-scroll">
<view
class="category-item {{currentType == item.id ? 'active' : ''}}"
wx:for="{{categoryList}}"
wx:key="id"
bindtap="changeCategory"
data-type="{{item.id}}"
>
{{item.name}}
</view>
</scroll-view>
</view>
<!-- 菜品列表 -->
<scroll-view scroll-y class="caipin-scroll">
<view class="caipin-item" wx:for="{{caipinList}}" wx:key="id">
<!-- 菜品图片 -->
<image class="caipin-photo" src="{{item.caipin_photo}}" mode="widthFix"></image>
<!-- 菜品信息 -->
<view class="caipin-info">
<view class="caipin-name">{{item.caipin_name}}</view>
<view class="caipin-desc">{{item.caipin_content}}</view>
<view class="caipin-price">
<text class="new-money">¥{{item.caipin_new_money}}</text>
<text class="old-money" wx:if="{{item.caipin_old_money > item.caipin_new_money}}">¥{{item.caipin_old_money}}</text>
</view>
<!-- 库存提示 -->
<view class="stock-tip" wx:if="{{item.caipin_kucun_number < 10 && item.caipin_kucun_number > 0}}">
仅剩{{item.caipin_kucun_number}}份
</view>
<view class="stock-tip sold-out" wx:if="{{item.caipin_kucun_number == 0}}">
已售罄
</view>
</view>
<!-- 加减按钮 -->
<view class="count-btn" wx:if="{{item.caipin_kucun_number > 0}}">
<view class="minus-btn" bindtap="minusCount" data-id="{{item.id}}" wx:if="{{cartMap[item.id] > 0}}">-</view>
<view class="count" wx:if="{{cartMap[item.id] > 0}}">{{cartMap[item.id]}}</view>
<view class="plus-btn" bindtap="plusCount" data-id="{{item.id}}">+</view>
</view>
</view>
</scroll-view>
<!-- 购物车悬浮按钮 -->
<view class="cart-float" bindtap="goToCart">
<image src="/images/cart.png" class="cart-icon"></image>
<view class="cart-count" wx:if="{{totalCount > 0}}">{{totalCount}}</view>
<view class="cart-money">¥{{totalMoney}}</view>
<view class="order-btn">去结算</view>
</view>
5.1.2 菜品列表页面逻辑(pages/caipin/list.js)
Page({
data: {
categoryList: [], // 菜品分类列表
caipinList: [], // 菜品列表
currentType: 1, // 当前选中的分类(默认“小笼包”)
cartMap: {}, // 购物车映射(key:菜品ID,value:数量)
totalCount: 0, // 购物车总数量
totalMoney: 0 // 购物车总金额
},
// 页面加载时获取分类与菜品数据
onLoad(options) {
this.getCategoryList();
this.getCaipinList(this.data.currentType);
// 从本地缓存获取购物车数据
const cartMap = wx.getStorageSync('cartMap') || {};
this.setData({ cartMap });
this.calculateCart();
},
// 获取菜品分类列表
getCategoryList() {
wx.request({
url: 'https://your-domain.com/api/category/list', // 后端接口地址
method: 'GET',
success: (res) => {
if (res.data.success) {
this.setData({ categoryList: res.data.data });
} else {
wx.showToast({ title: res.data.msg, icon: 'none' });
}
}
});
},
// 根据分类获取菜品列表
getCaipinList(type) {
wx.request({
url: 'https://your-domain.com/api/caipin/list',
method: 'GET',
data: { type },
success: (res) => {
if (res.data.success) {
this.setData({ caipinList: res.data.data });
} else {
wx.showToast({ title: res.data.msg, icon: 'none' });
}
}
});
},
// 切换菜品分类
changeCategory(e) {
const type = e.currentTarget.dataset.type;
this.setData({ currentType: type });
this.getCaipinList(type);
},
// 增加菜品数量
plusCount(e) {
const id = e.currentTarget.dataset.id;
const { cartMap, caipinList } = this.data;
// 查找当前菜品的库存
const caipin = caipinList.find(item => item.id === id);
if (caipin.caipin_kucun_number <= (cartMap[id] || 0)) {
wx.showToast({ title: '库存不足', icon: 'none' });
return;
}
// 更新购物车数量
cartMap[id] = (cartMap[id] || 0) + 1;
this.setData({ cartMap });
// 计算购物车总数量与金额
this.calculateCart();
// 保存购物车到本地缓存
wx.setStorageSync('cartMap', cartMap);
},
// 减少菜品数量
minusCount(e) {
const id = e.currentTarget.dataset.id;
const { cartMap } = this.data;
if (cartMap[id] <= 1) {
delete cartMap[id];
} else {
cartMap[id]--;
}
this.setData({ cartMap });
this.calculateCart();
wx.setStorageSync('cartMap', cartMap);
},
// 计算购物车总数量与金额
calculateCart() {
const { cartMap, caipinList } = this.data;
let totalCount = 0;
let totalMoney = 0;
// 遍历购物车,累加数量与金额
for (const id in cartMap) {
const count = cartMap[id];
const caipin = caipinList.find(item => item.id === id) ||
this.data.categoryList.flatMap(cat => cat.caipinList).find(item => item.id === id);
if (caipin) {
totalCount += count;
totalMoney += count * caipin.caipin_new_money;
}
}
this.setData({ totalCount, totalMoney: totalMoney.toFixed(2) });
},
// 跳转到购物车页面
goToCart() {
wx.navigateTo({ url: '/pages/cart/cart' });
}
});
5.2 Web 管理端核心功能实现(以 “订单管理” 为例)
5.2.1 订单管理页面(admin/order/list.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>订单管理 - 灌汤小笼包饭店点菜系统</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.min.css">
<script src="${pageContext.request.contextPath}/js/jquery-3.6.0.min.js"></script>
<style>
.order-status { padding: 2px 8px; border-radius: 4px; font-size: 12px; }
.status-0 { background-color: #ffc107; color: #fff; } /* 待接单 */
.status-1 { background-color: #17a2b8; color: #fff; } /* 已接单 */
.status-2 { background-color: #28a745; color: #fff; } /* 制作中 */
.status-3 { background-color: #6c757d; color: #fff; } /* 已完成 */
.status-4 { background-color: #dc3545; color: #fff; } /* 已取消 */
.order-item { margin-top: 10px; padding: 10px; border: 1px solid #eee; border-radius: 4px; }
</style>
</head>
<body>
<div class="container mt-3">
<!-- 页面标题与筛选栏 -->
<div class="row mb-3">
<div class="col-md-6">
<h3>订单管理</h3>
</div>
<div class="col-md-6 text-right">
<div class="form-inline justify-content-end">
<select class="form-control mr-2" id="orderStatus">
<option value="-1">所有状态</option>
<option value="0">待接单</option>
<option value="1">已接单</option>
<option value="2">制作中</option>
<option value="3">已完成</option>
<option value="4">已取消</option>
</select>
<input type="text" class="form-control mr-2" id="orderNo" placeholder="输入订单号搜索">
<button class="btn btn-primary" onclick="searchOrder()">搜索</button>
</div>
</div>
</div>
<!-- 订单列表 -->
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>订单号</th>
<th>桌号</th>
<th>客户</th>
<th>总金额</th>
<th>订单状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="orderTableBody">
<c:forEach items="${orderList}" var="order">
<tr>
<td>${order.orderUuidNumber}</td>
<td>${order.zhuohao}</td>
<td>
<img src="${order.customerAvatarUrl}" style="width: 30px; height: 30px; border-radius: 50%;">
${order.customerNickname}
</td>
<td>¥${order.totalMoney}</td>
<td>
<span class="order-status status-${order.orderStatus}">
<c:choose>
<c:when test="${order.orderStatus == 0}">待接单</c:when>
<c:when test="${order.orderStatus == 1}">已接单</c:when>
<c:when test="${order.orderStatus == 2}">制作中</c:when>
<c:when test="${order.orderStatus == 3}">已完成</c:when>
<c:when test="${order.orderStatus == 4}">已取消</c:when>
</c:choose>
</span>
</td>
<td><fmt:formatDate value="${order.createTime}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
<td>
<button class="btn btn-sm btn-info" onclick="viewOrderDetail(${order.id})">查看详情</button>
<c:if test="${order.orderStatus == 0}">
<button class="btn btn-sm btn-success ml-1" onclick="confirmOrder(${order.id})">确认接单</button>
<button class="btn btn-sm btn-danger ml-1" onclick="cancelOrder(${order.id})">取消订单</button>
</c:if>
<c:if test="${order.orderStatus == 1}">
<button class="btn btn-sm btn-warning ml-1" onclick="startMake(${order.id})">开始制作</button>
</c:if>
<c:if test="${order.orderStatus == 2}">
<button class="btn btn-sm btn-secondary ml-1" onclick="finishOrder(${order.id})">完成送餐</button>
</c:if>
</td>
</tr>
<!-- 订单详情(默认隐藏,点击“查看详情”显示) -->
<tr id="detail-${order.id}" style="display: none;">
<td colspan="7">
<div class="order-item">
<h5>订单项</h5>
<table class="table table-sm">
<thead>
<tr>
<th>菜品名称</th>
<th>数量</th>
<th>单价</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<c:forEach items="${order.orderItemList}" var="item">
<tr>
<td>${item.caipinName}</td>
<td>${item.buyNumber}</td>
<td>¥${item.caipinPrice}</td>
<td>¥${item.subtotalMoney}</td>
</tr>
</c:forEach>
</tbody>
</table>
<div class="text-right">
<span class="font-weight-bold">支付状态:</span>
<c:if test="${order.payStatus == 1}">
<span class="text-success">已支付(<fmt:formatDate value="${order.payTime}" pattern="HH:mm:ss"/>)</span>
</c:if>
<c:if test="${order.payStatus == 0}">
<span class="text-danger">未支付</span>
</c:if>
</div>
</div>
</td>
</tr>
</c:forEach>
</tbody>
</table>
<!-- 分页控件 -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item ${currentPage == 1 ? 'disabled' : ''}">
<a class="page-link" href="javascript:;" onclick="changePage(${currentPage - 1})">上一页</a>
</li>
<c:forEach begin="1" end="${totalPages}" var="page">
<li class="page-item ${currentPage == page ? 'active' : ''}">
<a class="page-link" href="javascript:;" onclick="changePage(${page})">${page}</a>
</li>
</c:forEach>
<li class="page-item ${currentPage == totalPages ? 'disabled' : ''}">
<a class="page-link" href="javascript:;" onclick="changePage(${currentPage + 1})">下一页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<!-- 订单状态修改弹窗 -->
<div class="modal fade" id="statusModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">确认接单</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<input type="hidden" id="orderId">
<input type="hidden" id="targetStatus">
<p id="modalContent">确定要接下该订单吗?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="submitStatusChange()">确认</button>
</div>
</div>
</div>
</div>
<script src="${pageContext.request.contextPath}/js/bootstrap.min.js"></script>
<script>
// 查看订单详情
function viewOrderDetail(orderId) {
const detailElement = document.getElementById(`detail-${orderId}`);
if (detailElement.style.display === 'none') {
detailElement.style.display = 'table-row';
} else {
detailElement.style.display = 'none';
}
}
// 确认接单(打开弹窗)
function confirmOrder(orderId) {
document.getElementById('orderId').value = orderId;
document.getElementById('targetStatus').value = 1;
document.getElementById('modalTitle').innerText = '确认接单';
document.getElementById('modalContent').innerText = '确定要接下该订单吗?接单后将自动扣减菜品库存。';
$('#statusModal').modal('show');
}
// 开始制作
function startMake(orderId) {
document.getElementById('orderId').value = orderId;
document.getElementById('targetStatus').value = 2;
document.getElementById('modalTitle').innerText = '开始制作';
document.getElementById('modalContent').innerText = '确定已开始制作该订单的菜品吗?';
$('#statusModal').modal('show');
}
// 完成送餐
function finishOrder(orderId) {
document.getElementById('orderId').value = orderId;
document.getElementById('targetStatus').value = 3;
document.getElementById('modalTitle').innerText = '完成送餐';
document.getElementById('modalContent').innerText = '确定已将菜品送至客户餐桌吗?';
$('#statusModal').modal('show');
}
// 取消订单
function cancelOrder(orderId) {
document.getElementById('orderId').value = orderId;
document.getElementById('targetStatus').value = 4;
document.getElementById('modalTitle').innerText = '取消订单';
document.getElementById('modalContent').innerText = '确定要取消该订单吗?取消后将自动恢复菜品库存。';
$('#statusModal').modal('show');
}
// 提交订单状态修改
function submitStatusChange() {
const orderId = document.getElementById('orderId').value;
const targetStatus = document.getElementById('targetStatus').value;
$.ajax({
url: '${pageContext.request.contextPath}/admin/order/updateStatus',
type: 'POST',
data: { orderId, targetStatus },
success: function(res) {
if (res.success) {
$('#statusModal').modal('hide');
location.reload(); // 刷新页面
} else {
alert(res.msg);
}
},
error: function() {
alert('网络错误,请重试');
}
});
}
// 搜索订单
function searchOrder() {
const orderStatus = document.getElementById('orderStatus').value;
const orderNo = document.getElementById('orderNo').value.trim();
window.location.href = `?orderStatus=${orderStatus}&orderNo=${orderNo}&page=1`;
}
// 切换分页
function changePage(page) {
const orderStatus = document.getElementById('orderStatus').value;
const orderNo = document.getElementById('orderNo').value.trim();
window.location.href = `?orderStatus=${orderStatus}&orderNo=${orderNo}&page=${page}`;
}
// 实时刷新订单(每30秒刷新一次)
setInterval(function() {
const orderStatus = document.getElementById('orderStatus').value;
const orderNo = document.getElementById('orderNo').value.trim();
const currentPage = ${currentPage};
$.ajax({
url: '${pageContext.request.contextPath}/admin/order/refresh',
type: 'GET',
data: { orderStatus, orderNo, page: currentPage },
success: function(res) {
if (res.success) {
document.getElementById('orderTableBody').innerHTML = res.data;
}
}
});
}, 30000);
</script>
</body>
</html>
5.2.2 后端订单状态修改接口(OrderController.java)
package com.xiaolongbao.controller.admin;
import com.xiaolongbao.pojo.CaipinOrder;
import com.xiaolongbao.service.CaipinOrderService;
import com.xiaolongbao.service.CaipinService;
import com.xiaolongbao.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
@RequestMapping("/admin/order")
public class OrderController {
@Autowired
private CaipinOrderService orderService;
@Autowired
private CaipinService caipinService;
/**
* 更新订单状态
* @param orderId 订单ID
* @param targetStatus 目标状态(1=已接单/2=制作中/3=已完成/4=已取消)
* @return 操作结果
*/
@PostMapping("/updateStatus")
@ResponseBody
public Result updateOrderStatus(
@RequestParam Integer orderId,
@RequestParam Integer targetStatus) {
try {
// 1. 查询订单信息
CaipinOrder order = orderService.getOrderById(orderId);
if (order == null) {
return Result.error("订单不存在");
}
// 2. 校验状态流转合法性(如“待接单”只能转“已接单”或“已取消”)
if (!isStatusValid(order.getOrderStatus(), targetStatus)) {
return Result.error("状态流转不合法");
}
// 3. 更新订单状态
order.setOrderStatus(targetStatus);
orderService.updateOrder(order);
// 4. 处理库存(接单时扣减库存,取消时恢复库存)
if (targetStatus == 1) { // 确认接单:扣减库存
caipinService.reduceStock(order.getOrderItemList());
} else if (targetStatus == 4) { // 取消订单:恢复库存
caipinService.restoreStock(order.getOrderItemList());
}
// 5. 推送消息给小程序端(通过WebSocket)
orderService.pushOrderStatusToWeixin(orderId, targetStatus);
return Result.success("订单状态更新成功");
} catch (Exception e) {
e.printStackTrace();
return Result.error("更新失败:" + e.getMessage());
}
}
/**
* 校验订单状态流转是否合法
* @param currentStatus 当前状态
* @param targetStatus 目标状态
* @return 合法返回true,否则返回false
*/
private boolean isStatusValid(Integer currentStatus, Integer targetStatus) {
// 状态流转规则:0→1/4;1→2/4;2→3/4;3→无;4→无
switch (currentStatus) {
case 0:
return targetStatus == 1 || targetStatus == 4;
case 1:
return targetStatus == 2 || targetStatus == 4;
case 2:
return targetStatus == 3 || targetStatus == 4;
case 3:
case 4:
return false; // 已完成/已取消的订单不能再修改状态
default:
return false;
}
}
}
六、系统测试:功能验证与性能评估
6.1 核心功能测试
通过 “等价类划分法” 设计测试用例,验证系统关键功能的正确性:
| 测试功能 | 测试用例 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| 小程序点菜 | 1. 点选库存充足的菜品(如 “小笼包 2 笼”);2. 点选库存为 0 的菜品;3. 取消已下单的菜品 | 1. 下单成功,库存扣减 2;2. 提示 “已售罄”,无法下单;3. 取消成功,库存恢复 | 与预期一致 | 是 |
| 订单状态更新 | 1. 待接单→已接单;2. 已接单→制作中;3. 制作中→已完成;4. 待接单→已取消 | 1. 库存扣减,小程序收到 “已接单” 通知;2. 小程序收到 “制作中” 通知;3. 小程序收到 “已完成” 通知;4. 库存恢复,小程序收到 “已取消” 通知 | 与预期一致 | 是 |
| 库存预警 | 1. 菜品库存低于 10 份;2. 菜品库存为 0 | 1. 管理端显示 “库存预警” 提示;2. 小程序端菜品标记 “已售罄” | 与预期一致 | 是 |
| 数据统计 | 1. 统计当日订单量;2. 统计热门菜品(按销量排序) | 1. 统计结果与实际订单数一致;2. 热门菜品排序正确(如 “鲜肉小笼包” 排第一) | 与预期一致 | 是 |
6.2 性能测试
通过 JMeter 工具模拟 100 个客户同时使用小程序点菜,测试系统关键接口性能:
| 接口名称 | 测试场景(100 用户并发) | 平均响应时间 | QPS(每秒请求量) | 成功率 |
|---|---|---|---|---|
| 菜品列表接口 | 加载小笼包分类菜品 | 0.4 秒 | 200 | 100% |
| 下单接口 | 提交包含 3 个菜品的订单 | 0.8 秒 | 120 | 100% |
| 订单状态接口 | 实时查询订单状态 | 0.3 秒 | 250 | 100% |
测试结果表明,系统在 100 用户并发场景下,所有接口响应时间均<1 秒,成功率 100%,满足中小型餐饮门店的高峰期需求。
七、附:核心资料获取
完整开发资料包含:
- 微信小程序端源码(页面文件、逻辑代码、配置文件);
- 后端源码(SSM 配置文件、Controller/Service/Mapper 层代码、核心工具类);
- MySQL 数据库脚本(创建表 SQL、测试数据 SQL);
- 操作手册(商家管理端使用指南、小程序端用户指南、常见问题解答);
- 部署文档(服务器部署步骤、小程序发布流程)。
👉 关注博主,可获取系统相关技术文档与核心代码,助力灌汤小笼包饭店点菜系统开发或毕设落地。
如果本文对你的微信小程序开发、餐饮系统设计有帮助,欢迎点赞 + 收藏 + 关注,后续会分享更多 “技术 + 餐饮 / 零售场景” 的实战案例!