引言

Spring Boot 3 作为 Java 后端的绝对主力,凭借 JDK 17+ 的原生支持、AOT 编译能力和 GraalVM 原生镜像,启动速度比 2.x 快了近 40%。而 微信云开发 则让小程序前端彻底摆脱了"自己搭服务器"的噩梦——云函数、云数据库、云存储三位一体,开发效率直接翻倍。这套组合拳打出来的点餐系统,后端扛并发,前端零运维,堪称 2026 年中小餐饮数字化的最优解。本文将从零开始,手把手带你搭建一套完整的点餐系统,包含源码级的核心实现。

源码及演示:s.ymzan.top

系统架构设计

整体采用 前后端分离 + 云开发混合架构

层级 技术选型 职责
前端(用户端) 微信小程序 + 云开发 菜品浏览、下单、支付、订单查询
前端(管理端) Vue 3 + Element Plus 菜品管理、订单管理、数据统计
后端服务 Spring Boot 3 + MyBatis-Plus 业务逻辑、权限控制、支付回调
数据库 MySQL 8.0 持久化存储
缓存 Redis 7.x 热点数据缓存、分布式锁、Session
消息队列 可选 RabbitMQ 订单异步处理、削峰填谷

核心数据流:

用户小程序 → 微信云函数(鉴权/轻量逻辑)→ Spring Boot 3(核心业务)→ MySQL/Redis

为什么不全用云开发?因为涉及支付回调、复杂订单状态机、多端管理后台这些重逻辑,云函数的执行时长和灵活性都不够。Spring Boot 3 负责"重活",云开发负责"快活",各司其职。

环境搭建与项目初始化

1. 开发环境

工具 版本要求
JDK 17+(Spring Boot 3 最低要求)
Maven 3.6+
MySQL 8.0.17+
Redis 7.x
Node.js 16+(小程序开发)
微信开发者工具 最新稳定版

2. Spring Boot 3 项目初始化

使用 Spring Initializr(start.spring.io)创建项目,添加以下依赖:

<dependencies>
    <!-- Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.5</version>
    </dependency>
    <!-- MySQL -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    <!-- 微信 SDK -->
    <dependency>
        <groupId>com.github.binarywang</groupId>
        <artifactId>weixin-java-mp</artifactId>
        <version>4.5.0</version>
    </dependency>
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.3</version>
    </dependency>
</dependencies>

3. application.yml 核心配置

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ordering_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
  data:
    redis:
      host: localhost
      port: 6379
      password: 
      database: 0

wechat:
  mp:
    app-id: your_app_id
    secret: your_secret

jwt:
  secret: your-256-bit-secret-key-here
  expiration: 86400000  # 24小时

后端核心实现(Spring Boot 3)

1. 项目目录结构

com.example.ordering
├── config/          # Redis配置、安全配置、跨域配置
├── controller/      # API控制器
├── entity/          # 数据库实体
├── mapper/          # MyBatis-Plus Mapper
├── service/         # 业务逻辑层
├── dto/             # 数据传输对象
├── util/            # 工具类(JWT、分布式锁等)
└── exception/       # 全局异常处理

2. 数据库设计

这是整个系统的地基,设计不好后面全是坑。

菜品信息表(dish_info)

字段 类型 说明
dish_id VARCHAR(32) 主键
dish_name VARCHAR(50) 菜品名称
price DECIMAL(10,2) 价格
category VARCHAR(20) 分类
image_url VARCHAR(255) 图片链接
description TEXT 描述
status TINYINT(1) 1上架/0下架
create_time DATETIME 创建时间

订单信息表(order_info)

字段 类型 说明
order_id VARCHAR(32) 主键
user_id VARCHAR(32) 用户ID
dish_list TEXT 菜品列表(JSON)
total_amount DECIMAL(10,2) 总金额
payment_status TINYINT(1) 1已支付/0未支付
order_status TINYINT(1) 0待付款/1已付款/2制作中/3已完成/4已取消
create_time DATETIME 创建时间
finish_time DATETIME 完成时间

用户表(user)

字段 类型 说明
id BIGINT 主键自增
openid VARCHAR(64) 微信OpenID
nickname VARCHAR(32) 昵称
phone VARCHAR(11) 手机号
create_time DATETIME 注册时间

3. 微信登录——整个系统的入口

微信登录是点餐系统的第一道门。2024年微信官方政策已明确:新注册公众号需完成企业认证才能获得接口权限,个人订阅号接口能力大幅受限。所以务必使用已认证的服务号

核心流程:

小程序端 wx.login() → 获取 code → 后端调用 auth.code2Session → 获取 openid + session_key → 生成 JWT token → 返回给前端

SignUtil.java — 签名验证工具

@Component
public class SignUtil {
    public static boolean checkSignature(String signature, String timestamp, String nonce) {
        String[] arr = new String[]{"your_token", timestamp, nonce};
        Arrays.sort(arr);
        StringBuilder content = new StringBuilder();
        for (String s : arr) {
            content.append(s);
        }
        String tmpStr = DigestUtils.sha1Hex(content.toString());
        return tmpStr != null && tmpStr.equals(signature);
    }
}

WeChatController.java — 微信接入验证

@RestController
@RequestMapping("/wechat")
public class WeChatController {

    @Autowired
    private WeChatService weChatService;

    @GetMapping("/message")
    public String validate(@RequestParam String signature,
                           @RequestParam String timestamp,
                           @RequestParam String nonce,
                           @RequestParam String echostr) {
        if (SignUtil.checkSignature(signature, timestamp, nonce)) {
            return echostr;
        }
        return "error";
    }

    @PostMapping("/login")
    public Result login(@RequestBody LoginDTO dto) {
        WxMaJscode2SessionResult session = weChatService.getSessionInfo(dto.getCode());
        String openid = session.getOpenid();
        String token = weChatService.loginOrRegister(openid);
        return Result.success(Map.of("token", token, "userInfo", weChatService.getUserInfo(openid)));
    }
}

JWT 工具类

@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String secret;

    public String generateToken(Long userId, String username) {
        return Jwts.builder()
                .subject(username)
                .claim("userId", userId)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + 86400000))
                .signWith(Keys.hmacShaKeyFor(secret.getBytes()))
                .compact();
    }

    public Claims parseToken(String token) {
        return Jwts.parser()
                .verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
}

4. 菜品管理模块

DishController.java

@RestController
@RequestMapping("/api/dish")
public class DishController {

    @Autowired
    private DishService dishService;

    @GetMapping("/list")
    public Result list(@RequestParam(required = false) String category) {
        return Result.success(dishService.listByCategory(category));
    }

    @GetMapping("/{id}")
    public Result detail(@PathVariable String id) {
        return Result.success(dishService.getById(id));
    }

    @PostMapping("/admin/add")
    @PreAuthorize("hasRole('ADMIN')")
    public Result add(@RequestBody DishDTO dto) {
        dishService.save(dto);
        return Result.success("添加成功");
    }
}

DishService.java 核心逻辑

@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public List<Dish> listByCategory(String category) {
        String cacheKey = "dish:list:" + (category != null ? category : "all");
        // 先查缓存
        List<Dish> cached = (List<Dish>) redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }
        // 缓存未命中,查数据库
        LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Dish::getStatus, 1);
        if (StringUtils.isNotBlank(category)) {
            wrapper.eq(Dish::getCategory, category);
        }
        List<Dish> list = this.list(wrapper);
        // 写入缓存,30分钟过期
        redisTemplate.opsForValue().set(cacheKey, list, 30, TimeUnit.MINUTES);
        return list;
    }
}

这里用 Redis 缓存了菜品列表,这是点餐系统的性能命脉——高峰期每秒几百次的菜品查询,全部打到 MySQL 上直接宕机。

5. 订单模块——最复杂也最核心

订单创建涉及三个关键问题:库存扣减的并发安全支付回调的幂等性订单状态机的流转控制

5.1 分布式锁防止超卖

这是点餐系统的技术难点。多个用户同时点同一道菜,库存只剩 1 份,必须保证只有一个人能下单成功。

错误写法一:先 setnx 再 expire(有死锁风险)

Long result = jedis.setnx(key, value);
if (result == 1) {
    jedis.expire(key, expireTime);  // 如果这里程序崩溃,锁永远不过期 → 死锁
}

正确写法:原子命令 setnx + 过期时间一步到位(Spring Boot 3 + Redis)

@Component
public class RedisLockUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean tryLock(String key, String value, long expireSeconds) {
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    public void unlock(String key, String value) {
        String script = "if redis.call('get',KEYS[1])==ARGV[1] then " +
                        "return redis.call('del',KEYS[1]) else return 0 end";
        redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(key),
                value
        );
    }
}

OrderService.java — 下单核心逻辑

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RedisLockUtil redisLockUtil;

    @Autowired
    private DishMapper dishMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(CreateOrderDTO dto) {
        String lockKey = "dish:stock:" + dto.getDishId();
        String lockValue = UUID.randomUUID().toString();

        try {
            // 获取分布式锁,最多等待3秒,锁自动10秒后释放
            if (!redisLockUtil.tryLock(lockKey, lockValue, 10)) {
                throw new BusinessException("系统繁忙,请稍后重试");
            }

            // 查库存
            Dish dish = dishMapper.selectById(dto.getDishId());
            if (dish == null || dish.getStock() < dto.getQuantity()) {
                throw new BusinessException("库存不足");
            }

            // 扣库存
            dish.setStock(dish.getStock() - dto.getQuantity());
            dishMapper.updateById(dish);

            // 生成订单
            Order order = new Order();
            order.setOrderId(IdUtil.fastSimpleUUID());
            order.setUserId(dto.getUserId());
            order.setDishList(JSON.toJSONString(dto.getDishList()));
            order.setTotalAmount(dto.getTotalAmount());
            order.setPaymentStatus(0);
            order.setOrderStatus(0);
            order.setCreateTime(new Date());
            this.save(order);

            return order.getOrderId();

        } finally {
            redisLockUtil.unlock(lockKey, lockValue);
        }
    }
}

这段代码的关键在于:锁的粒度是每道菜,而不是整个订单表。这样不同菜品之间的下单互不影响,并发能力直接拉满。

5.2 支付回调幂等处理

微信支付回调会发多次通知,必须保证同一笔订单只处理一次。

@PostMapping("/pay/notify")
public String payNotify(@RequestBody String xmlData) {
    try {
        Map<String, String> map = WxPayUtil.xmlToMap(xmlData);
        String orderId = map.get("out_trade_no");

        // 幂等检查:先查订单状态
        Order order = this.getById(orderId);
        if (order.getPaymentStatus() == 1) {
            return WxPayUtil.successXml();  // 已经处理过,直接返回成功
        }

        // 验证金额
        if (!order.getTotalAmount().equals(new BigDecimal(map.get("total_fee")).divide(new BigDecimal(100)))) {
            throw new BusinessException("金额不一致");
        }

        // 更新订单状态
        order.setPaymentStatus(1);
        order.setOrderStatus(1);
        order.setFinishTime(new Date());
        this.updateById(order);

        return WxPayUtil.successXml();
    } catch (Exception e) {
        log.error("支付回调处理失败", e);
        return WxPayUtil.failXml();
    }
}

6. 全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result handleBusiness(BusinessException e) {
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }
}

前端实现:微信小程序 + 云开发

1. 为什么用云开发?

对于点餐系统的前端,80% 的需求是"查菜品、下订单、查订单",这些都是 CRUD 操作,云开发的云数据库 + 云函数天然适配。

2.项目结构

miniprogram/
├── app.js
├── app.json
├── app.wxss
├── pages/
│   ├── index/          # 首页(菜品列表)
│   ├── dish/           # 菜品详情
│   ├── order/          # 订单确认
│   ├── orderList/      # 订单列表
│   └── mine/           # 个人中心
├── cloud/              # 云函数
│   ├── login/          # 微信登录
│   └── createOrder/    # 创建订单(轻量逻辑)
└── utils/
    └── request.js      # 封装请求

3. app.json 配置

{
  "pages": [
    "pages/index/index",
    "pages/dish/dish",
    "pages/order/order",
    "pages/orderList/orderList",
    "pages/mine/mine"
  ],
  "window": {
    "navigationBarBackgroundColor": "#07c160",
    "navigationBarTitleText": "美味点餐"
  },
  "cloud": true,
  "subpackages": [
    {
      "root": "pages/order",
      "pages": ["confirm/confirm"]
    }
  ]
}

4. 首页——菜品列表(云数据库查询)

// pages/index/index.js
const app = getApp()
const db = wx.cloud.database()

Page({
  data: {
    categories: ['热销', '主食', '小炒', '凉菜', '饮品'],
    currentCategory: '热销',
    dishes: []
  },

  onLoad() {
    this.loadDishes()
  },

  async loadDishes() {
    wx.showLoading({ title: '加载中' })
    try {
      const res = await db.collection('dishes')
        .where({ status: 1, category: this.data.currentCategory })
        .orderBy('sales', 'desc')
        .limit(20)
        .get()
      this.setData({ dishes: res.data })
    } catch (e) {
      console.error(e)
    }
    wx.hideLoading()
  },

  switchCategory(e) {
    this.setData({ currentCategory: e.currentTarget.dataset.cat })
    this.loadDishes()
  }
})

5. 下单——调用 Spring Boot 后端

// pages/order/order.js
const app = getApp()

Page({
  data: {
    cart: [],
    totalAmount: 0
  },

  async submitOrder() {
    if (this.data.cart.length === 0) {
      wx.showToast({ title: '请先选菜', icon: 'none' })
      return
    }

    wx.showLoading({ title: '提交订单' })
    try {
      const res = await wx.cloud.callFunction({
        name: 'createOrder',
        data: {
          dishList: this.data.cart,
          totalAmount: this.data.totalAmount
        }
      })

      if (res.result.code === 0) {
        // 调起微信支付
        const payParams = res.result.payParams
        wx.requestPayment({
          ...payParams,
          success() {
            wx.showToast({ title: '下单成功' })
            wx.redirectTo({ url: '/pages/orderList/orderList' })
          }
        })
      }
    } catch (e) {
      wx.showToast({ title: '下单失败', icon: 'none' })
    }
    wx.hideLoading()
  }
})

cloud/createOrder/index.js — 云函数

const cloud = require('wx-server-sdk')
const axios = require('axios')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })

exports.main = async (event) => {
  const { dishList, totalAmount } = event
  const wxContext = cloud.getWXContext()
  const openid = wxContext.OPENID

  // 调用 Spring Boot 后端创建订单
  const res = await axios.post('https://your-domain.com/api/order/create', {
    userId: openid,
    dishList: dishList,
    totalAmount: totalAmount
  })

  if (res.data.code === 0) {
    // 获取支付参数
    const payRes = await axios.post('https://your-domain.com/api/pay/create', {
      orderId: res.data.data.orderId,
      openid: openid,
      amount: totalAmount
    })
    return { code: 0, payParams: payRes.data.data }
  }
  return { code: -1, message: '创建订单失败' }
}

部署与性能优化

1. Docker 部署

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/ordering-system.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - mysql
      - redis
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: your_password
      MYSQL_DATABASE: ordering_system
    volumes:
      - mysql_data:/var/lib/mysql
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  mysql_data:

2. 关键优化点

优化项 方案 效果
菜品列表缓存 Redis 30分钟过期 QPS 提升 10 倍
下单并发控制 Redis 分布式锁(按菜品粒度) 杜绝超卖
支付回调幂等 先查状态再处理 避免重复入账
小程序分包 主包 < 2MB,分包异步加载 首屏加载 < 1s
图片存储 阿里云 OSS + CDN 图片加载 < 200ms

在这里插入图片描述

总结

Spring Boot 3 解决了点餐系统最难啃的骨头——并发锁等、订单状态机,必须用成熟的 Java 生态兜底。微信云开发则把前端从"搭服务器、配域名、搞运维"的泥潭里解放出来,让开发者把精力花在用户体验上,而不是基础设施上。技术会迭代,但架构思想不会过时。2026年再看这套系统,它依然适用于奶茶店、快餐店、食堂等绝大多数中小餐饮场景。真正决定系统成败的,从来不是用了多新的框架,而是你有没有把并发、幂等、缓存这三件事做扎实。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐