1. 项目背景与需求分析
"苍穹外卖Day03"这个标题看起来像是一个系列开发日志的第三篇。根据常见的项目命名习惯,这很可能是一个外卖平台的后端开发实战记录。作为系列教程的第三天内容,通常会聚焦在核心业务功能的实现上。
在典型的外卖系统开发中,第三天往往需要完成以下关键任务:
- 用户下单流程的完整实现
- 订单状态机设计与流转
- 餐厅接单与配送分配逻辑
- 基础数据统计功能
提示:实际开发中,Day03通常会遇到业务逻辑复杂度的第一个高峰,需要特别注意事务处理和异常场景的覆盖。
2. 技术栈选型与项目结构
基于当前主流的外卖系统技术方案,推荐采用以下技术组合:
java复制// 典型的技术栈示例
Spring Boot 2.7.x + MyBatis-Plus + Redis + MySQL 8.0
项目目录结构建议如下:
code复制src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── cangqiong/
│ │ ├── config/ # 配置类
│ │ ├── controller/ # 控制器
│ │ ├── dto/ # 数据传输对象
│ │ ├── entity/ # 数据库实体
│ │ ├── enums/ # 枚举类
│ │ ├── mapper/ # MyBatis映射
│ │ ├── service/ # 服务层
│ │ └── utils/ # 工具类
│ └── resources/
│ ├── mapper/ # XML映射文件
│ ├── static/ # 静态资源
│ ├── templates/ # 模板文件
│ └── application.yml
3. 核心业务模块实现
3.1 订单状态机设计
外卖订单的典型状态流转如下:
| 状态 | 触发条件 | 后续动作 |
|---|---|---|
| 待支付 | 用户提交订单 | 启动15分钟倒计时 |
| 已支付 | 支付成功回调 | 通知餐厅接单 |
| 已接单 | 餐厅确认接单 | 分配骑手 |
| 配送中 | 骑手取餐 | 开始配送计时 |
| 已完成 | 用户确认收货 | 结算金额 |
| 已取消 | 用户/系统取消 | 退款处理 |
状态机实现建议使用枚举+策略模式:
java复制public enum OrderStatus {
PENDING_PAYMENT {
@Override
public boolean canChangeTo(OrderStatus newStatus) {
return newStatus == PAID || newStatus == CANCELLED;
}
},
// 其他状态定义...
}
3.2 下单业务逻辑实现
核心下单流程应包含以下步骤:
- 校验用户地址有效性
- 计算商品总价与配送费
- 检查餐厅营业状态
- 生成唯一订单号
- 创建订单主表记录
- 批量插入订单商品明细
- 扣减库存(需要分布式锁)
- 发送延迟消息检查支付超时
关键代码示例:
java复制@Transactional
public OrderDTO createOrder(OrderCreateDTO createDTO) {
// 参数校验
validateOrder(createDTO);
// 价格计算
BigDecimal totalAmount = calculateTotal(createDTO);
// 构建订单实体
Order order = buildOrderEntity(createDTO, totalAmount);
// 保存订单
orderMapper.insert(order);
// 保存订单明细
batchInsertItems(order.getId(), createDTO.getItems());
// 扣减库存
reduceInventory(createDTO.getItems());
// 发送延迟消息
sendDelayMessage(order.getId());
return convertToDTO(order);
}
4. 典型问题与解决方案
4.1 分布式事务处理
外卖系统常见的分布式事务场景:
- 下单时扣减库存与创建订单的一致性
- 支付成功回调与订单状态更新的原子性
推荐解决方案:
- 本地消息表+定时任务补偿
- 使用Seata AT模式
- 基于RocketMQ的事务消息
4.2 高并发场景优化
针对秒杀等场景的优化策略:
- 商品库存采用Redis缓存+预扣减
- 订单号生成使用雪花算法
- 热点数据分离:将订单主表与扩展表分开
- 读写分离:查询走从库
缓存设计示例:
java复制public boolean tryLockInventory(Long dishId, Integer count) {
String key = "inventory_lock:" + dishId;
String value = String.valueOf(System.currentTimeMillis());
// 使用SETNX实现分布式锁
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, value, 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
try {
// 执行库存扣减逻辑
return deductInventory(dishId, count);
} finally {
// 释放锁
redisTemplate.delete(key);
}
}
return false;
}
5. 数据统计功能实现
5.1 实时订单看板
使用Redis的zset实现实时订单统计:
java复制// 订单创建时记录
public void addToOrderBoard(Long orderId) {
String today = LocalDate.now().toString();
String key = "order:stats:" + today;
redisTemplate.opsForZSet().add(key, orderId.toString(), System.currentTimeMillis());
}
// 获取今日订单数
public Long getTodayOrderCount() {
String today = LocalDate.now().toString();
String key = "order:stats:" + today;
return redisTemplate.opsForZSet().size(key);
}
5.2 销售额统计
采用定时任务+批处理的方式:
- 每小时统计一次销售额
- 使用MyBatis的批量插入
- 建立统计结果缓存
sql复制<!-- 销售额统计SQL -->
<select id="statSales" resultType="com.cangqiong.dto.SalesStatsDTO">
SELECT
DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') AS time_point,
SUM(actual_amount) AS sales_amount,
COUNT(*) AS order_count
FROM orders
WHERE create_time >= #{startTime}
AND create_time < #{endTime}
GROUP BY time_point
</select>
6. 测试与部署建议
6.1 单元测试重点
需要特别关注的测试场景:
- 订单取消后的库存回滚
- 支付超时订单的自动关闭
- 餐厅停业时的下单拦截
- 优惠券使用与退款处理
测试示例:
java复制@Test
public void testOrderCancel() {
// 准备测试数据
Long orderId = createTestOrder();
// 执行取消操作
orderService.cancelOrder(orderId, "用户取消");
// 验证结果
Order order = orderMapper.selectById(orderId);
assertEquals(OrderStatus.CANCELLED, order.getStatus());
assertNotNull(order.getCancelReason());
// 验证库存回滚
List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
items.forEach(item -> {
Dish dish = dishMapper.selectById(item.getDishId());
assertTrue(dish.getStock() >= item.getNumber());
});
}
6.2 性能优化配置
生产环境推荐配置:
yaml复制server:
tomcat:
max-threads: 200
min-spare-threads: 20
spring:
datasource:
hikari:
maximum-pool-size: 30
connection-timeout: 30000
redis:
lettuce:
pool:
max-active: 50
max-idle: 20
7. 扩展功能建议
完成基础功能后,可以考虑实现:
- 智能配送路线规划
- 用户行为分析推荐
- 餐厅评分动态计算
- 多维度数据大屏
配送算法伪代码示例:
code复制function assignRider(order):
riders = getAvailableRiders(order.restaurant)
if riders.empty:
return false
bestRider = null
minScore = Infinity
for rider in riders:
score = calculateAssignmentScore(rider, order)
if score < minScore:
minScore = score
bestRider = rider
if bestRider:
createDeliveryTask(bestRider, order)
return true
return false
在实际开发中,Day03通常会遇到业务复杂度陡增的情况。我的经验是:先确保主流程畅通,再处理异常分支;先保证功能正确性,再考虑性能优化;关键业务操作必须添加完善的日志记录。特别是在订单状态变更时,建议记录完整的操作日志,这对后续排查问题非常有帮助。
