1. 项目概述:从零搭建外卖平台的技术探索
去年参与的一个餐饮SaaS项目让我深刻体会到,外卖平台的技术架构远比表面看到的复杂得多。这个"sky-take-out"系列正是基于实战经验的技术复盘,Day01我们将从最基础的环境搭建开始,逐步拆解一个现代化外卖系统的技术栈。不同于简单的CRUD应用,外卖系统需要处理高并发订单、实时地理位置、多端数据同步等特殊场景,这对技术选型和架构设计提出了独特挑战。
2. 技术栈选型与核心考量
2.1 后端技术决策
选择SpringBoot作为基础框架并非偶然。在对比了Node.js和Python Flask后,我们发现Java生态在以下方面具有优势:
- 成熟的分布式事务解决方案(如Seata)
- 完善的OAuth2安全实现
- 对高并发场景下连接池管理的精细控制
- 与主流消息队列(Kafka/RocketMQ)的无缝集成
特别值得注意的是,我们采用了SpringCloud Alibaba而非原生SpringCloud,主要考虑到:
- Nacos在配置管理和服务发现上比Eureka更符合国内团队的使用习惯
- Sentinel对突发流量的控制比Hystrix更直观
- 阿里云生态的兼容性降低了后期部署成本
2.2 前端技术适配
采用Vue3+TypeScript的组合出于三点考虑:
- Composition API更适合复杂业务逻辑的封装
- TypeScript的强类型检查能减少跨团队协作的接口问题
- Vite的快速热更新显著提升开发体验
针对移动端特殊需求,我们额外引入了:
- 高德地图JS API(餐饮类目专用版本)
- WebSocket实时订单状态推送
- 自定义的滑动加载组件(优化菜单列表性能)
3. 开发环境搭建实战
3.1 基础设施准备
# 使用Docker Compose快速搭建依赖服务 version: '3' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: sky123 ports: - "3306:3306" volumes: - ./mysql/data:/var/lib/mysql redis: image: redis:6 ports: - "6379:6379" command: redis-server --requirepass sky123关键提示:MySQL必须配置为ROW格式的binlog,这是后续实现CDC(变更数据捕获)的基础。在my.cnf中添加:
[mysqld] log-bin=mysql-bin binlog-format=ROW server_id=1
3.2 项目骨架生成
使用Spring Initializr创建项目时,这些依赖必不可少:
- Spring Web(REST接口)
- MyBatis Plus(数据库操作)
- Lombok(代码简化)
- SpringDoc OpenAPI(API文档)
- SpringCloud Starter Alibaba Nacos(服务注册)
前端项目初始化时特别注意:
npm create vite@latest sky-take-out-web --template vue-ts cd sky-take-out-web npm install axios vue-router@4 pinia @element-plus/icons-vue4. 核心模块设计要点
4.1 领域模型划分
外卖系统通常包含以下核心领域:
- 商户管理(含资质审核)
- 商品服务(SKU管理)
- 订单系统(状态机设计)
- 配送调度(地理围栏算法)
- 支付对账(分布式事务)
我们采用DDD(领域驱动设计)的模块划分方式:
src/ ├── main/ │ ├── java/ │ │ └── com.sky.takeout/ │ │ ├── merchant/ # 商户领域 │ │ ├── product/ # 商品领域 │ │ ├── order/ # 订单领域 │ │ └── delivery/ # 配送领域4.2 订单状态机设计
外卖订单的复杂状态流转需要精确定义:
public enum OrderStatus { PENDING_PAYMENT, // 待支付 PAID, // 已支付 MERCHANT_ACCEPTED, // 商家接单 COOKING, // 制作中 DELIVERY_PICKUP, // 骑手取货 DELIVERING, // 配送中 COMPLETED, // 已完成 CANCELLED // 已取消 }使用状态模式实现状态转换:
public interface OrderState { void next(Order order); void prev(Order order); void cancel(Order order); } @Component @RequiredArgsConstructor public class PaidState implements OrderState { private final PaymentService paymentService; @Override public void next(Order order) { order.setStatus(OrderStatus.MERCHANT_ACCEPTED); } @Override public void cancel(Order order) { paymentService.refund(order); order.setStatus(OrderStatus.CANCELLED); } }5. 典型问题排查实录
5.1 跨商户缓存污染
现象:A商户登录后看到B商户的商品数据 原因:Redis键设计未包含tenantId 解决方案:
// 错误写法 String key = "product:" + productId; // 正确写法 String key = String.format("tenant:%s:product:%s", tenantId, productId);5.2 订单超时未关闭
排查路径:
- 检查定时任务日志
- 确认Redis延迟队列是否正常
- 验证数据库事务隔离级别(应为READ_COMMITTED)
- 最终发现是消息TTL设置错误
修正方案:
// RabbitMQ消息配置 @Bean public Queue orderTimeoutQueue() { return QueueBuilder.durable("order.timeout.queue") .withArgument("x-message-ttl", 900000) // 15分钟 .withArgument("x-dead-letter-exchange", "order.dlx.exchange") .build(); }6. 性能优化关键策略
6.1 菜单加载加速
采用多级缓存策略:
- 浏览器本地缓存静态菜单JSON(有效期2小时)
- CDN缓存店铺头图等静态资源
- Redis缓存热门商品(LFU淘汰策略)
- MySQL查询优化:
-- 添加覆盖索引 ALTER TABLE product ADD INDEX idx_merchant_status (merchant_id, status);
6.2 订单创建优化
原始流程的问题:
- 串行校验(库存→优惠券→运费)
- 同步调用支付网关
- 全表扫描查询活动
优化后的并行处理:
CompletableFuture<Void> stockCheck = CompletableFuture.runAsync(() -> { inventoryService.check(items); }, stockThreadPool); CompletableFuture<Void> couponCheck = CompletableFuture.runAsync(() -> { couponService.validate(couponId); }, couponThreadPool); CompletableFuture.allOf(stockCheck, couponCheck).get(500, MILLISECONDS);7. 安全防护实践
7.1 防刷单机制
实现维度:
- 设备指纹识别(通过UA+IP+行为特征生成)
- 滑动验证码二次验证(下单频率超过阈值时触发)
- 基于LBS的异常检测(同一账号多地下单)
核心代码片段:
public void checkOrderFrequency(Long userId) { String key = "order:limit:" + userId; Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { redisTemplate.expire(key, 1, TimeUnit.HOURS); } if (count > 20) { throw new BusinessException("操作过于频繁"); } }7.2 敏感数据脱敏
在DTO层统一处理:
public class UserDTO { @JsonSerialize(using = PhoneDesensitizer.class) private String phone; @JsonSerialize(using = AddressDesensitizer.class) private String address; } public class PhoneDesensitizer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")); } }8. 监控体系建设
8.1 埋点设计
关键埋点位置:
- 订单创建成功率(含失败原因分类)
- 支付回调延迟(不同支付渠道对比)
- 商家接单响应时间(分时段统计)
使用Prometheus客户端示例:
@RestController public class OrderController { private final Counter orderCounter; public OrderController(MeterRegistry registry) { this.orderCounter = registry.counter("order.create.total", "channel", "app"); } @PostMapping("/orders") public Order createOrder() { orderCounter.increment(); // ... } }8.2 日志规范
采用结构化日志格式:
<Pattern> %d{yyyy-MM-dd HH:mm:ss} | %X{traceId} | %-5level | %logger{36} | %msg%n </Pattern>关键日志场景:
- 用户敏感操作(登录、修改手机号)
- 支付状态变更(需记录前后状态)
- 库存扣减异常(实际扣减值与预期值)