1. 项目背景与核心价值
"0x3f 第32天 外卖套餐项目自己写"这个标题乍看简单,但蕴含了多个值得深入探讨的技术维度。作为一个连续32天坚持编程实践的系列项目,这个外卖套餐系统开发案例特别适合想要提升全栈开发能力的中级开发者。
我在实际开发中发现,外卖系统看似简单,但涉及前后端分离架构、复杂状态管理、多端数据同步等典型工程问题。通过自主实现这样一个项目,可以系统性地掌握以下核心技能:
- 商品与套餐的数据关系建模
- 购物车与订单的状态流转设计
- 移动端与后台管理系统的权限隔离
- 高并发场景下的库存扣减策略
2. 技术架构设计
2.1 技术栈选型
经过多个同类项目的实践验证,我推荐以下技术组合:
前端:React + TypeScript + Vite 移动端:React Native或Uniapp跨端方案 后端:Spring Boot 3.x + MyBatis-Plus 数据库:MySQL 8.0 + Redis 7.0这个组合的优势在于:
- 前端采用现代构建工具Vite,开发体验远优于传统Webpack
- Spring Boot 3.x对GraalVM原生镜像的支持,为后期性能优化留有余地
- MyBatis-Plus的ActiveRecord模式能极大简化数据层代码
2.2 核心数据模型
外卖系统的实体关系需要特别注意套餐与单品的关联设计。我采用的设计方案是:
// 商品基础表 class Product { Long id; String name; BigDecimal price; Integer stock; // 库存字段需要原子操作 } // 套餐表 class Combo { Long id; String name; BigDecimal discount; // 套餐折扣 @TableField(exist = false) List<ComboItem> items; // 套餐包含项 } // 套餐项关联表 class ComboItem { Long comboId; Long productId; Integer quantity; // 包含商品数量 }关键提示:库存字段一定要用Integer而不是int,便于后续实现CAS乐观锁
3. 核心功能实现
3.1 购物车设计
购物车模块需要考虑以下几个特殊场景:
- 同一商品在多个套餐中的叠加计算
- 临时优惠券的实时价格计算
- 库存不足时的友好提示
我的实现方案采用Redux+immer的方案管理复杂状态:
// 购物车状态结构 interface CartState { items: Array<{ id: string; product?: Product; // 单品 combo?: Combo; // 套餐 quantity: number; selected: boolean; }>; discounts: Discount[]; } // 使用immer处理状态更新 const nextState = produce(state, draft => { const item = draft.items.find(x => x.id === action.payload.id); if(item) { item.quantity += action.payload.quantity; // 自动处理库存校验... } });3.2 订单并发控制
针对秒杀场景下的超卖问题,我采用分布式锁+库存预扣的方案:
- 用户下单时先获取Redis分布式锁
- 查询库存并生成预扣记录
- 创建订单主表数据
- 异步进行实际库存扣减
核心锁实现逻辑:
public boolean tryLock(String key, long expireSeconds) { String lockKey = "lock:" + key; return redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", expireSeconds, TimeUnit.SECONDS); } // 使用示例 if(lockUtil.tryLock("order:"+userId, 10)) { try { // 处理订单逻辑 } finally { redisTemplate.delete("lock:order:"+userId); } }4. 性能优化实践
4.1 缓存策略设计
针对外卖系统读多写少的特性,我设计了多级缓存方案:
- 静态菜单数据:CDN全量缓存
- 商品详情:Redis缓存+本地缓存
- 价格信息:Redis原子计数器
缓存更新采用Tag版本号机制:
UPDATE product SET price = 38.5, cache_version = cache_version + 1 WHERE id = 123前端请求时携带最后已知的version,服务端比对后决定是否返回新数据。
4.2 SQL优化案例
套餐查询的N+1问题是典型性能瓶颈。通过MyBatis-Plus的@TableField注解配合自定义SQL解决:
<select id="selectCombosWithItems" resultMap="comboResultMap"> SELECT c.*, ci.product_id, ci.quantity FROM combo c LEFT JOIN combo_item ci ON c.id = ci.combo_id WHERE c.status = 1 ORDER BY c.sort DESC </select>配合ResultMap的嵌套集合映射,实现单次查询加载所有关联数据。
5. 踩坑经验实录
5.1 浮点数精度问题
外卖系统涉及大量金额计算,必须避免使用float/double。我的解决方案:
- 数据库统一使用DECIMAL(10,2)
- 后端使用BigDecimal类型
- 前端使用number-precision库处理计算
// 错误示范 double price = 0.1 + 0.2; // 0.30000000000000004 // 正确做法 BigDecimal total = new BigDecimal("0.1") .add(new BigDecimal("0.2"));5.2 移动端适配问题
在React Native中实现商品列表时,发现FlatList在安卓机上的性能问题。最终优化方案:
- 使用getItemLayout固定行高
- 配合windowSize参数控制渲染范围
- 复杂卡片使用React.memo包装
<FlatList data={products} getItemLayout={(data, index) => ( {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} )} windowSize={5} renderItem={({item}) => <MemoizedProductCard item={item} />} />6. 项目扩展方向
完成基础功能后,可以考虑以下进阶改造:
- 接入第三方配送API实现实时轨迹
- 使用WebSocket实现订单状态推送
- 基于用户画像的智能推荐
- 利用GIS数据优化配送范围计算
其中配送范围判断的几何算法值得单独说明:
// 使用JTS库判断点是否在多边形内 GeometryFactory gf = new GeometryFactory(); Coordinate[] coordinates = ...; // 店铺配送范围坐标 Polygon deliveryArea = gf.createPolygon(coordinates); Point userLocation = gf.createPoint( new Coordinate(lng, lat) ); boolean canDelivery = deliveryArea.contains(userLocation);这个项目最让我有成就感的是解决了高并发下的库存一致性问题。通过将数据库乐观锁与Redis分布式锁结合,最终在压力测试中实现了2000+ TPS的订单处理能力。建议大家在实现基础功能后,一定要用JMeter等工具进行压力测试,才能真正掌握系统瓶颈所在。