Skip to content
第 248 / 250 章架构⏱ 18 分钟阅读

第 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 + 长连接推送
计价时段 + 距离 + 调价
安全一键报警 + 行程分享

动手练习

  1. 用 Redis Geo 实现附近 5km 司机查询
  2. 设计一个简单的派单算法
  3. 模拟位置上报的 Kafka 流处理
  4. 调研滴滴的派单策略

下一章:下一章:第 249 章:综合项目实战 - AI 客服系统

本站基于 VitePress 构建 · 由 Codebook 团队维护