微信小程序实战:灌汤小笼包饭店点菜系统设计与实现(含完整源码 + 数据库脚本)

201 阅读18分钟

一、项目背景:餐饮行业的点菜痛点与数字化需求

在传统灌汤小笼包饭店的运营中,点菜环节常面临三大核心痛点:

  1. 效率低下:服务员手写菜单、人工传菜,高峰期易出现漏单、错单,客户等待时间长;
  2. 信息滞后:菜品库存实时更新不及时,常出现 “客户点餐后才告知售罄” 的尴尬场景;
  3. 管理混乱:人工统计订单数据繁琐,无法快速分析热门菜品、客户消费习惯,影响运营决策。

为解决这些问题,本项目开发 “基于微信小程序的灌汤小笼包饭店点菜系统”,整合 “菜品展示、在线点菜、订单管理、库存同步、数据统计” 全流程功能。客户通过微信小程序即可完成点菜下单,商家通过后台实时处理订单,实现点菜环节的数字化、自动化,提升客户体验与运营效率。

二、核心技术栈:轻量高效的技术选型

系统围绕 “开发成本低、部署便捷、用户门槛低” 原则选型,技术栈覆盖小程序端、后端服务、数据存储,适配中小型餐饮门店的业务需求:

技术类别具体选型核心优势
小程序端微信小程序原生开发(WXML + WXSS + JS)无需下载安装,客户通过微信扫码即可使用,降低用户门槛;支持微信支付、地理位置等原生能力,适配餐饮场景的支付、门店定位需求;界面轻量化,加载速度快,高峰期操作流畅。
后端框架SSM(Spring + SpringMVC + MyBatis)Spring 实现事务管理(如 “下单后自动扣减库存”),确保数据一致性;SpringMVC 负责请求分发,适配小程序端与管理端的接口交互;MyBatis 支持自定义 SQL,灵活实现 “菜品筛选”“订单统计” 等复杂查询。
开发语言Java 11稳定兼容 SSM 框架,支持面向对象编程,代码可复用性高;具备成熟的异常处理机制,便于排查线上问题。
数据库MySQL 8.0开源免费,支持事务与索引(确保 “订单创建 - 库存更新” 原子性);支持百万级数据存储,满足门店每日订单、菜品数据的存储需求;适配 Windows/Linux 系统,部署灵活。
管理端前端JSP + Bootstrap + jQueryBootstrap 提供响应式布局,适配电脑 / 平板的管理端操作;jQuery 简化 DOM 操作与 AJAX 异步请求(如实时刷新订单状态);JSP 支持动态页面渲染,便于展示订单、库存等实时数据。
开发工具微信开发者工具 + IntelliJ IDEA微信开发者工具支持小程序实时预览、调试;IDEA 支持 SSM 代码提示、断点调试,集成 Maven 管理依赖,提升后端开发效率。
辅助技术Redis + EasyExcelRedis 缓存热门菜品数据(如 “销量 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 客户点菜流程

  1. 进入小程序:客户扫描门店桌贴二维码,进入灌汤小笼包饭店点菜小程序(自动定位当前桌号);
  2. 浏览菜品:小程序展示菜品分类(如 “招牌小笼包”“凉菜”“汤品”),客户可查看菜品图片、价格、库存,点击 “加入购物车”;
  3. 确认订单:客户进入购物车,调整菜品数量(如 “小笼包 2 笼”“酸辣汤 1 碗”),点击 “提交订单”;
  4. 在线支付:支持微信支付,支付完成后小程序生成订单号,同时向商家后台推送订单通知;
  5. 订单跟踪:客户可在 “我的订单” 中查看订单状态(“待接单”“制作中”“已完成”),避免反复催促服务员。

3.2.2 商家订单处理流程

  1. 接收订单:商家后台实时弹出新订单提醒(声音 + 弹窗),显示桌号、菜品、数量、支付状态;
  2. 确认接单:店员点击 “确认接单”,系统自动扣减对应菜品库存(如 “小笼包库存 - 2”);
  3. 制作菜品:后厨通过打印机自动打印订单小票,按小票制作菜品;
  4. 完成送餐:菜品制作完成后,店员点击 “已送餐”,小程序向客户推送 “菜品已送达” 通知;
  5. 订单归档:客户用餐结束后,系统自动将订单标记为 “已完成”,支持导出 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)
序号列名数据类型说明允许空
1IdInt(11)主键 ID
2caipin_nameVarchar(100)菜品名称(如 “鲜肉灌汤小笼包”)
3caipin_photoVarchar(255)菜品图片 URL
4caipin_typesInt(11)菜品分类(1 = 小笼包 / 2 = 凉菜 / 3 = 汤品)
5caipin_kucun_numberInt(11)菜品库存数量
6caipin_old_moneyDecimal(10,2)菜品原价(单位:元,用于折扣展示)
7caipin_new_moneyDecimal(10,2)菜品现价(单位:元)
8caipin_salesInt(11)菜品销量(用于热门排序)
9shangxia_typesInt(11)是否上架(0 = 下架 / 1 = 上架)
10caipin_contentText菜品描述(如 “皮薄馅大,汤汁浓郁”)
11create_timeTimestamp创建时间
表 4-2 订单表(caipin_order)
序号列名数据类型说明允许空
1IdInt(11)主键 ID
2order_uuid_numberVarchar(50)订单号(如 “OD20251115001”)
3weixin_openidVarchar(100)客户微信 openid(关联客户信息)
4zhuohaoVarchar(20)桌号(如 “1 号桌”“包间 A”)
5total_moneyDecimal(10,2)订单总金额(单位:元)
6pay_statusInt(11)支付状态(0 = 未支付 / 1 = 已支付)
7order_statusInt(11)订单状态(0 = 待接单 / 1 = 已接单 / 2 = 制作中 / 3 = 已完成 / 4 = 已取消)
8pay_timeTimestamp支付时间
9create_timeTimestamp订单创建时间
表 4-3 订单项表(order_item)
序号列名数据类型说明允许空
1IdInt(11)主键 ID
2order_idInt(11)订单 ID(关联订单表主键)
3caipin_idInt(11)菜品 ID(关联菜品表主键)
4buy_numberInt(11)购买数量(如 “2” 代表 2 笼)
5caipin_priceDecimal(10,2)购买时的菜品单价(单位:元)
6subtotal_moneyDecimal(10,2)订单项小计金额(单价 × 数量)
7create_timeTimestamp创建时间
表 4-4 客户表(customer)
序号列名数据类型说明允许空
1IdInt(11)主键 ID
2weixin_openidVarchar(100)微信 openid(唯一标识)
3nicknameVarchar(50)客户微信昵称
4avatar_urlVarchar(255)客户微信头像 URL
5phoneVarchar(20)客户手机号(可选填写)
6create_timeTimestamp首次使用时间
表 4-5 门店表(store)
序号列名数据类型说明允许空
1IdInt(11)主键 ID
2store_nameVarchar(100)门店名称(如 “XX 灌汤小笼包总店”)
3store_addressVarchar(255)门店地址
4business_hoursVarchar(100)营业时间(如 “06:30-21:00”)
5phoneVarchar(20)门店联系电话
6store_photoVarchar(255)门店图片 URL
7create_timeTimestamp创建时间

五、系统实现:核心功能代码与界面展示

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">&times;</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. 菜品库存为 01. 管理端显示 “库存预警” 提示;2. 小程序端菜品标记 “已售罄”与预期一致
数据统计1. 统计当日订单量;2. 统计热门菜品(按销量排序)1. 统计结果与实际订单数一致;2. 热门菜品排序正确(如 “鲜肉小笼包” 排第一)与预期一致

6.2 性能测试

通过 JMeter 工具模拟 100 个客户同时使用小程序点菜,测试系统关键接口性能:

接口名称测试场景(100 用户并发)平均响应时间QPS(每秒请求量)成功率
菜品列表接口加载小笼包分类菜品0.4 秒200100%
下单接口提交包含 3 个菜品的订单0.8 秒120100%
订单状态接口实时查询订单状态0.3 秒250100%

测试结果表明,系统在 100 用户并发场景下,所有接口响应时间均<1 秒,成功率 100%,满足中小型餐饮门店的高峰期需求。

七、附:核心资料获取

完整开发资料包含:

  • 微信小程序端源码(页面文件、逻辑代码、配置文件);
  • 后端源码(SSM 配置文件、Controller/Service/Mapper 层代码、核心工具类);
  • MySQL 数据库脚本(创建表 SQL、测试数据 SQL);
  • 操作手册(商家管理端使用指南、小程序端用户指南、常见问题解答);
  • 部署文档(服务器部署步骤、小程序发布流程)。

👉 关注博主,可获取系统相关技术文档与核心代码,助力灌汤小笼包饭店点菜系统开发或毕设落地。

如果本文对你的微信小程序开发、餐饮系统设计有帮助,欢迎点赞 + 收藏 + 关注,后续会分享更多 “技术 + 餐饮 / 零售场景” 的实战案例!