第 248 章:综合项目实战 - 网约车调度系统
学习目标
- 实时调度系统设计
- 地理空间索引
- 高并发派单算法
- 安全与风控
一、系统架构
二、地理空间
2.1 Redis Geo
java
@Service
public class DriverLocationService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String DRIVER_GEO_KEY = "driver:geo:online";
/**
* 司机上线/位置更新
*/
public void updateLocation(Long driverId, double lng, double lat) {
redisTemplate.opsForGeo().add(DRIVER_GEO_KEY,
new Point(lng, lat),
driverId.toString()
);
}
/**
* 查找附近司机
*/
public List<NearbyDriver> findNearby(double lng, double lat, double radiusKm) {
GeoResults<GeoReference<String>> results = redisTemplate.opsForGeo()
.radius(DRIVER_GEO_KEY,
new Circle(new Point(lng, lat), new Distance(radiusKm, Metrics.KILOMETERS)));
return results.getContent().stream()
.map(r -> new NearbyDriver(
Long.parseLong(r.getContent().getName()),
r.getDistance().getValue(),
r.getContent().getPoint()
))
.collect(Collectors.toList());
}
/**
* 司机下线
*/
public void offline(Long driverId) {
redisTemplate.opsForGeo().remove(DRIVER_GEO_KEY, driverId.toString());
}
}2.2 网格索引(高并发优化)
yaml
geo:
grid:
size: 0.01 度(约 1 公里)
storage:
- grid:{lng}:{lat} -> SET<driverId>
- driver:{driverId} -> {lng, lat, status}
查询附近司机:
1. 算当前格子及周边 8 格
2. 从 Redis 取这 9 个格子的司机 ID 集合
3. 过滤、距离计算、排序三、订单流程
3.1 乘客下单
3.2 订单实体
java
@Data
@Entity
@Table(name = "t_order")
public class RideOrder {
@Id
private Long id;
private String orderNo;
private Long passengerId;
private Long driverId;
private Double startLng;
private Double startLat;
private String startAddress;
private Double endLng;
private Double endLat;
private String endAddress;
private Integer carType; // 1:经济 2:舒适 3:商务
private OrderStatus status; // CREATED, DISPATCHING, ACCEPTED, ARRIVED, STARTED, FINISHED, CANCELED
private BigDecimal estimatedAmount;
private BigDecimal actualAmount;
private LocalDateTime createdAt;
private LocalDateTime acceptedAt;
private LocalDateTime arrivedAt;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
}
public enum OrderStatus {
CREATED, // 待支付
DISPATCHING, // 派单中
ACCEPTED, // 已接单
ARRIVED, // 司机到达
STARTED, // 行程中
FINISHED, // 行程结束
PAID, // 已支付
CANCELED // 已取消
}四、派单算法
4.1 派单策略
4.2 派单服务
java
@Service
@Slf4j
public class DispatchService {
@Autowired
private DriverLocationService locationService;
@Autowired
private KafkaTemplate<String, DispatchEvent> kafkaTemplate;
private static final double[] RADII = {0.5, 1, 2, 3, 5}; // 公里
/**
* 派单
*/
public DispatchResult dispatch(RideOrder order) {
for (double radius : RADII) {
List<NearbyDriver> drivers = locationService.findNearby(
order.getStartLng(),
order.getStartLat(),
radius
);
// 过滤可用司机(空闲、车型符合)
List<NearbyDriver> candidates = drivers.stream()
.filter(d -> isAvailable(d.getDriverId()))
.filter(d -> matchCarType(d.getDriverId(), order.getCarType()))
.sorted(Comparator.comparing(NearbyDriver::getDistance))
.collect(Collectors.toList());
if (candidates.isEmpty()) {
continue;
}
// 派单给前 N 个(广播)
int maxBroadcast = candidates.size() > 5 ? 5 : candidates.size();
for (int i = 0; i < maxBroadcast; i++) {
NearbyDriver driver = candidates.get(i);
offerToDriver(driver.getDriverId(), order);
}
// 等司机抢单,30 秒后没人接再扩大范围
return DispatchResult.broadcasting(order.getId(), candidates.size());
}
return DispatchResult.noDriver();
}
/**
* 派单给司机
*/
private void offerToDriver(Long driverId, RideOrder order) {
// 推送给司机
Driver driver = driverService.findById(driverId);
channelManager.sendToUser(driverId, new OfferMessage(order));
// 设置抢单超时
orderOfferService.setOfferTimeout(order.getId(), driverId, 30);
}
/**
* 司机抢单
*/
public boolean grab(Long driverId, Long orderId) {
// 1. 抢单锁
String lockKey = "grab:" + orderId;
RLock lock = redisson.getLock(lockKey);
try {
if (!lock.tryLock(0, 5, TimeUnit.SECONDS)) {
return false;
}
// 2. 乐观锁更新(防并发抢单)
int rows = orderMapper.tryGrab(orderId, driverId, OrderStatus.DISPATCHING);
return rows > 0;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
}
}4.3 抢单 SQL
sql
-- 乐观锁,只有状态为 DISPATCHING 的订单可被抢
UPDATE t_order
SET driver_id = ?, status = 'ACCEPTED', accepted_at = NOW()
WHERE id = ? AND status = 'DISPATCHING' AND driver_id IS NULL五、计价与支付
5.1 计价规则
java
@Service
public class PriceCalculator {
/**
* 计算车费
*/
public BigDecimal calculate(RideOrder order, Long distance, Long duration) {
PricingRule rule = pricingRuleRepo.findByCityAndType(order.getCity(), order.getCarType());
BigDecimal amount = BigDecimal.ZERO;
amount = amount.add(rule.getBaseFare()); // 起价
if (distance > rule.getBaseDistance()) {
amount = amount.add(rule.getPerKmPrice()
.multiply(BigDecimal.valueOf(distance - rule.getBaseDistance())));
}
if (duration > rule.getBaseMinutes()) {
amount = amount.add(rule.getPerMinutePrice()
.multiply(BigDecimal.valueOf(duration - rule.getBaseMinutes())));
}
// 时段系数
BigDecimal factor = timeFactor(order.getStartedAt());
amount = amount.multiply(factor);
// 动态调价倍数
if (order.getSurgeMultiplier() != null && order.getSurgeMultiplier() > 1) {
amount = amount.multiply(BigDecimal.valueOf(order.getSurgeMultiplier()));
}
// 最低收费
if (amount.compareTo(rule.getMinFare()) < 0) {
amount = rule.getMinFare();
}
return amount.setScale(2, RoundingMode.HALF_UP);
}
}5.2 动态调价
yaml
动态调价:
触发条件:
- 供小于求:周围 3km 内空闲司机 < 订单数 * 0.7
- 高峰时段:上下班、节假日
调价倍数:
- 轻度: 1.2
- 中度: 1.5
- 重度: 2.0
- 极端: 3.0六、行程跟踪
6.1 实时位置
6.2 位置上报
java
@RestController
@RequestMapping("/api/location")
public class LocationController {
@Autowired
private KafkaTemplate<String, LocationMessage> kafkaTemplate;
/**
* 司机位置上报(高频)
*/
@PostMapping("/report")
public Result<?> report(@RequestBody LocationReportRequest req) {
// 1. 校验
if (req.getDriverId() == null || req.getLng() == null || req.getLat() == null) {
return Result.error("参数错误");
}
// 2. 上传到 Kafka(异步)
LocationMessage msg = new LocationMessage();
msg.setDriverId(req.getDriverId());
msg.setLng(req.getLng());
msg.setLat(req.getLat());
msg.setSpeed(req.getSpeed());
msg.setDirection(req.getDirection());
msg.setTimestamp(System.currentTimeMillis());
kafkaTemplate.send("driver-location", msg);
return Result.ok();
}
}6.3 异常检测
java
@Component
public class LocationAnomalyDetector {
/**
* 检测司机位置异常(防止作弊)
*/
public void detect(Long driverId, LocationMessage msg) {
LocationMessage last = lastLocationRepo.findLast(driverId);
if (last == null) return;
double distance = GeoUtils.distance(
last.getLng(), last.getLat(),
msg.getLng(), msg.getLat()
);
long timeDiff = (msg.getTimestamp() - last.getTimestamp()) / 1000;
if (timeDiff > 0) {
double speed = distance / timeDiff * 3.6; // km/h
if (speed > 200) {
log.warn("司机 {} 异常速度 {} km/h", driverId, speed);
// 标记待审核
anomalyRepo.save(new LocationAnomaly(driverId, "speed", speed, msg));
}
}
}
}七、安全与风控
7.1 一键报警
java
@RestController
@RequestMapping("/api/safety")
public class SafetyController {
@Autowired
private EmergencyService emergencyService;
/**
* 乘客一键报警
*/
@PostMapping("/sos")
public Result<?> sos(@RequestBody SosRequest req) {
// 1. 立即通知
emergencyService.triggerAlarm(req.getOrderId(),
req.getLat(), req.getLng(),
req.getUserId());
// 2. 录制音频(如果支持)
recordingService.startRecording(req.getOrderId());
// 3. 联系司机
driverService.callDriver(req.getDriverId(), "乘客报警");
return Result.ok("已报警");
}
}
@Service
public class EmergencyService {
public void triggerAlarm(Long orderId, Double lat, Double lng, Long userId) {
// 1. 通知客服系统
crmService.createEmergencyOrder(orderId, userId);
// 2. 上报警方接口
policeService.reportEmergency(orderId, lat, lng, userId);
// 3. 通知平台
alertService.alertInternal(EmergencyLevel.CRITICAL,
"订单 {} 用户报警", orderId);
// 4. 联系紧急联系人
List<String> contacts = emergencyContactRepo.findByUserId(userId);
for (String phone : contacts) {
smsService.send(phone, "您的紧急联系人发起报警");
}
}
}7.2 行程分享
java
@RestController
@RequestMapping("/api/trip")
public class TripShareController {
/**
* 生成行程分享链接
*/
@PostMapping("/share")
public ShareResponse share(@RequestBody ShareRequest req) {
String code = IdUtil.fastSimpleUUID().substring(0, 8);
TripShare share = new TripShare();
share.setCode(code);
share.setOrderId(req.getOrderId());
share.setUserId(req.getUserId());
share.setExpireAt(LocalDateTime.now().plusHours(2));
tripShareRepo.save(share);
return new ShareResponse("https://trip.example.com/s/" + code);
}
}
/**
* 公开访问接口
*/
@RestController
@RequestMapping("/api/public/trip")
public class PublicTripController {
@GetMapping("/s/{code}")
public TripVO publicView(@PathVariable String code) {
TripShare share = tripShareRepo.findByCode(code);
if (share == null || share.getExpireAt().isBefore(LocalDateTime.now())) {
throw new BizException("链接已失效");
}
// 实时位置
return tripService.getCurrentLocation(share.getOrderId());
}
}八、风控
java
@Service
public class RiskControlService {
/**
* 订单风控检查
*/
public RiskResult check(OrderRequest request) {
RiskResult result = new RiskResult();
// 1. 设备指纹
if (deviceFingerprintService.isBlacklisted(request.getDeviceId())) {
result.setDeny(true);
result.setReason("设备黑名单");
return result;
}
// 2. 行为模式
int behavior = behaviorAnalyzer.analyze(request.getUserId());
if (behavior > 80) {
result.setDeny(true);
result.setReason("行为异常");
return result;
}
// 3. 频次限制
int count = orderCountService.countByUser(request.getUserId(), Duration.ofHours(1));
if (count > 10) {
result.setDeny(true);
result.setReason("短时间内订单过多");
return result;
}
return result;
}
}九、本章小结
| 模块 | 关键 |
|---|---|
| 位置 | Redis Geo / 网格索引 |
| 派单 | 广播抢单 + 乐观锁 |
| 实时 | Kafka + 长连接推送 |
| 计价 | 时段 + 距离 + 调价 |
| 安全 | 一键报警 + 行程分享 |
动手练习
- 用 Redis Geo 实现附近 5km 司机查询
- 设计一个简单的派单算法
- 模拟位置上报的 Kafka 流处理
- 调研滴滴的派单策略
下一章:下一章:第 249 章:综合项目实战 - AI 客服系统