外卖霸王餐API接口架构设计思路分析
对于开发者而言,构建一套高并发、高可用的外卖霸王餐API接口架构,是实现流量主与外卖平台(美团、饿了么)数据互通的关键。本文将基于俱美开放平台(http://www.baodanbao.com.cn)的技术实践,深入剖析外卖霸王餐系统的架构设计、核心代码实现及风控策略。
一、 系统架构设计
外卖霸王餐系统的核心在于“聚合”与“分润”。系统需要对接上游外卖平台的开放API,获取优惠券与订单数据,并通过下游API将数据分发给代理商。架构设计上采用分层模式:
- 接入层:Nginx负载均衡 + API网关,负责请求路由、鉴权与限流。
- 服务层:Spring Boot微服务,包含优惠券服务、订单服务、用户服务。
- 数据层:MySQL存储核心业务数据,Redis缓存热点优惠券与Token,RabbitMQ处理异步订单结算。
- 第三方对接层:封装美团、饿了么SDK,处理OAuth2.0授权与数据拉取。
二、 核心API接口开发
API接口是系统的门面,必须保证高性能与安全性。以下是一个获取“附近霸王餐列表”的Java接口示例。
首先,定义通用的响应实体:
package baodanbao.com.cn.common;
/**
* @author baodanbao.com.cn
* API统一返回结果
*/
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 200;
r.msg = "成功";
r.data = data;
return r;
}
public static Result<Void> error(String msg) {
Result<Void> r = new Result<>();
r.code = 500;
r.msg = msg;
return r;
}
// 省略getter/setter
}
接下来,实现优惠券查询接口。为了防止接口被恶意刷取,我们在Service层加入了基于IP的限流逻辑:
package baodanbao.com.cn.controller;
import baodanbao.com.cn.common.Result;
import baodanbao.com.cn.model.CouponDTO;
import baodanbao.com.cn.service.CouponService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author baodanbao.com.cn
* 霸王餐优惠券API控制器
*/
@RestController
@RequestMapping("/api/v1/coupon")
public class CouponController {
@Autowired
private CouponService couponService;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 获取附近霸王餐列表
* @param lat 纬度
* @param lng 经度
* @param ip 客户端IP(用于限流)
* @return 优惠券列表
*/
@GetMapping("/list")
public Result<List<CouponDTO>> getCouponList(
@RequestParam Double lat,
@RequestParam Double lng,
HttpServletRequest request) {
String ip = request.getRemoteAddr();
String limitKey = "coupon:limit:" + ip;
// 1. 基于Redis的滑动窗口限流 (每分钟最多10次)
Long count = redisTemplate.opsForValue().increment(limitKey, 1);
if (count == 1) {
redisTemplate.expire(limitKey, 1, TimeUnit.MINUTES);
}
if (count > 10) {
return Result.error("请求过于频繁,请稍后再试");
}
// 2. 调用业务层获取数据
List<CouponDTO> coupons = couponService.getNearbyCoupons(lat, lng);
return Result.success(coupons);
}
}
三、 CPS分润架构与订单同步
CPS架构的难点在于如何准确追踪订单归属并计算佣金。由于外卖平台通常不提供实时推送,我们采用“轮询+回调”的混合模式。
1. 订单实体设计
订单表必须记录推广渠道ID,以便结算时追溯佣金归属。
package baodanbao.com.cn.entity;
import java.math.BigDecimal;
import java.util.Date;
/**
* @author baodanbao.com.cn
* 外卖订单实体
*/
public class Order {
private Long id;
private String outTradeNo; // 外卖平台订单号
private Long userId; // 下单用户ID
private Long promoterId; // 推广员ID
private BigDecimal orderPrice; // 订单金额
private BigDecimal commissionRate; // 佣金比例
private BigDecimal commission; // 佣金金额
private Integer status; // 订单状态
private Date finishTime; // 完成时间
// 省略getter/setter
}
2. 订单同步与分润逻辑
使用Spring Task定时拉取上游平台的订单数据,并进行本地比对与更新。
package baodanbao.com.cn.task;
import baodanbao.com.cn.entity.Order;
import baodanbao.com.cn.mapper.OrderMapper;
import baodanbao.com.cn.service.PlatformSyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author baodanbao.com.cn
* 订单同步定时任务
*/
@Component
public class OrderSyncTask {
@Autowired
private PlatformSyncService platformSyncService; // 上游平台同步服务
@Autowired
private OrderMapper orderMapper;
/**
* 每5分钟同步一次美团和饿了么的订单
*/
@Scheduled(fixedRate = 5 * 60 * 1000)
public void syncOrders() {
// 1. 同步美团订单
List<Order> mtOrders = platformSyncService.fetchMeituanOrders();
// 2. 同步饿了么订单
List<Order> eleOrders = platformSyncService.fetchElemeOrders();
// 3. 处理订单
processOrders(mtOrders);
processOrders(eleOrders);
}
private void processOrders(List<Order> orders) {
for (Order remoteOrder : orders) {
Order localOrder = orderMapper.selectByOutTradeNo(remoteOrder.getOutTradeNo());
if (localOrder == null) {
// 新订单入库
orderMapper.insert(remoteOrder);
} else if (!localOrder.getStatus().equals(remoteOrder.getStatus())) {
// 状态变更
orderMapper.updateStatus(remoteOrder.getOutTradeNo(), remoteOrder.getStatus());
// 如果订单完成,触发分润
if (remoteOrder.getStatus() == OrderStatus.FINISH.getCode()) {
calculateCommission(remoteOrder);
}
}
}
}
/**
* 计算并结算佣金
* @param order 完成的订单
*/
private void calculateCommission(Order order) {
// 佣金 = 订单金额 * 佣金比例
BigDecimal commission = order.getOrderPrice().multiply(order.getCommissionRate());
order.setCommission(commission);
order.setFinishTime(new Date());
orderMapper.updateCommission(order);
// 调用资金服务,将佣金打入推广员账户
// fundService.credit(order.getPromoterId(), commission);
}
}
四、 高并发缓存策略
外卖霸王餐接口在饭点高峰期面临巨大的流量冲击。为了保证系统不被压垮,必须引入多级缓存策略。
1. Redis缓存热点数据
使用Redis缓存热门的霸王餐列表,减少对MySQL的直接查询。
package baodanbao.com.cn.service;
import baodanbao.com.cn.entity.Coupon;
import baodanbao.com.cn.mapper.CouponMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author baodanbao.com.cn
* 优惠券业务服务
*/
@Service
public class CouponService {
@Autowired
private CouponMapper couponMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public List<Coupon> getHotCoupons() {
String key = "coupons:hot";
// 1. 读取缓存
List<Coupon> coupons = (List<Coupon>) redisTemplate.opsForValue().get(key);
if (coupons != null) {
return coupons;
}
// 2. 缓存未命中,查询数据库
coupons = couponMapper.selectHot();
// 3. 写入缓存,设置随机过期时间防止雪崩
int expireTime = 600 + new Random().nextInt(300); // 10-15分钟
redisTemplate.opsForValue().set(key, coupons, expireTime, TimeUnit.SECONDS);
return coupons;
}
}
2. 本地缓存Guava Cache
对于极少变更的配置数据(如城市列表),使用本地缓存Guava Cache,进一步减少网络IO。
package baodanbao.com.cn.util;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author baodanbao.com.cn
* 本地缓存工具
*/
@Component
public class LocalCache {
// Guava本地缓存
private static final Cache<String, Object> CACHE = CacheBuilder.newBuilder()
.maximumSize(1000) // 最大缓存条数
.expireAfterWrite(30, TimeUnit.MINUTES) // 过期时间
.build();
public static void put(String key, Object value) {
CACHE.put(key, value);
}
public static Object get(String key) {
return CACHE.getIfPresent(key);
}
public static void invalidate(String key) {
CACHE.invalidate(key);
}
}
本文著作权归 俱美开放平台 ,转载请注明出处!

156

被折叠的 条评论
为什么被折叠?



