DispatchServiceImpl.java 16.3 KB
package com.diligrp.rider.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.diligrp.rider.common.enums.DispatchConditionType;
import com.diligrp.rider.entity.*;
import com.diligrp.rider.mapper.*;
import com.diligrp.rider.service.DispatchRuleService;
import com.diligrp.rider.service.DispatchService;
import com.diligrp.rider.service.WebhookService;
import com.diligrp.rider.util.GeoUtil;
import com.diligrp.rider.vo.DispatchRuleTemplateVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;

@Slf4j
@Service
@RequiredArgsConstructor
public class DispatchServiceImpl implements DispatchService {

    private final DispatchRuleService dispatchRuleService;
    private final RiderMapper riderMapper;
    private final RiderLocationMapper locationMapper;
    private final OrdersMapper ordersMapper;
    private final RiderOrderCountMapper countMapper;
    private final RiderOrderRefuseMapper refuseMapper;
    private final WebhookService webhookService;
    private final ObjectMapper objectMapper;

    @Override
    @Transactional
    public Long dispatch(Orders order) {
        DispatchRuleTemplateVO rule = dispatchRuleService.getActiveRule(order.getCityId());
        if (rule == null) {
            log.debug("城市 {} 无生效调度规则,跳过派单", order.getCityId());
            return null;
        }
        if (rule.getAutoDispatch() == null || rule.getAutoDispatch() != 1) {
            return null;
        }

        // 1. 获取候选骑手:同城市、在线、审核通过
        List<Rider> candidates = riderMapper.selectList(
                new LambdaQueryWrapper<Rider>()
                        .eq(Rider::getCityId, order.getCityId())
                        .eq(Rider::getIsRest, 0)
                        .eq(Rider::getUserStatus, 1)
                        .eq(Rider::getStatus, 1));
        if (candidates.isEmpty()) {
            log.debug("城市 {} 无在线骑手", order.getCityId());
            return null;
        }

        // 排除已拒单的骑手
        List<Long> refusedRiderIds = getRefusedRiderIds(order.getId());
        if (!refusedRiderIds.isEmpty()) {
            candidates.removeIf(r -> refusedRiderIds.contains(r.getId()));
        }

        // 排除转出该单的骑手
        if (order.getOldRiderId() != null && order.getOldRiderId() > 0) {
            candidates.removeIf(r -> r.getId().equals(order.getOldRiderId()));
        }

        if (candidates.isEmpty()) return null;

        // 2. 获取骑手位置
        List<Long> riderIds = candidates.stream().map(Rider::getId).toList();
        Map<Long, RiderLocation> locationMap = getLocationMap(riderIds);

        // 3. 获取启用的条件列表(按 sortOrder 升序)
        List<DispatchRuleTemplateVO.ConditionItem> enabledConditions = new ArrayList<>();
        if (rule.getConditions() != null) {
            for (DispatchRuleTemplateVO.ConditionItem c : rule.getConditions()) {
                if (c.getEnabled() != null && c.getEnabled() == 1) {
                    enabledConditions.add(c);
                }
            }
        }

        // 4. 获取订单取货点坐标
        double orderLat = parseDouble(order.getFLat());
        double orderLng = parseDouble(order.getFLng());
        double orderTLat = parseDouble(order.getTLat());
        double orderTLng = parseDouble(order.getTLng());

        // 5. 预加载每个骑手的统计数据
        Map<Long, Integer> currentLoadMap = getCurrentLoadMap(riderIds);
        Map<Long, Integer> dailyCountMap = getDailyCountMap(riderIds);

        // 6. 对每个候选骑手评分
        int totalConditions = enabledConditions.size();
        List<RiderScore> scores = new ArrayList<>();

        for (Rider rider : candidates) {
            RiderLocation loc = locationMap.get(rider.getId());
            if (loc == null) continue; // 无位置信息的骑手不参与派单

            double riderLat = parseDouble(loc.getLat());
            double riderLng = parseDouble(loc.getLng());
            double distanceToPickup = GeoUtil.calcDistanceKm(riderLat, riderLng, orderLat, orderLng);

            // 持单量检查(强制过滤)
            int currentLoad = currentLoadMap.getOrDefault(rider.getId(), 0);
            if (rule.getGrabMaxPerRider() != null && currentLoad >= rule.getGrabMaxPerRider()) {
                continue; // 持单已满,跳过
            }

            double score = 0;
            boolean filtered = false;

            for (int i = 0; i < enabledConditions.size(); i++) {
                DispatchRuleTemplateVO.ConditionItem cond = enabledConditions.get(i);
                double weight = totalConditions - i; // 排在前面的权重更高
                double condScore = evaluateCondition(cond, rider, loc, order,
                        distanceToPickup, riderLat, riderLng, orderLat, orderLng, orderTLat, orderTLng,
                        currentLoad, dailyCountMap.getOrDefault(rider.getId(), 0));

                if (condScore < 0) {
                    // 负分表示强制不通过(如距离超出阈值)
                    filtered = true;
                    break;
                }
                score += condScore * weight;
            }

            if (!filtered) {
                scores.add(new RiderScore(rider.getId(), score, distanceToPickup));
            }
        }

        if (scores.isEmpty()) {
            log.debug("订单 {} 无匹配骑手", order.getId());
            return null;
        }

        // 7. 选择最优骑手:分数最高,平分取距离最近
        scores.sort((a, b) -> {
            int cmp = Double.compare(b.score, a.score);
            if (cmp != 0) return cmp;
            return Double.compare(a.distance, b.distance);
        });

        Long bestRiderId = scores.get(0).riderId;

        // 8. 指派订单
        long now = System.currentTimeMillis() / 1000;
        int updated = ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
                .eq(Orders::getId, order.getId())
                .eq(Orders::getStatus, 2)
                .eq(Orders::getRiderId, 0)
                .set(Orders::getRiderId, bestRiderId)
                .set(Orders::getStatus, 3)
                .set(Orders::getGrapTime, now)
                .set(Orders::getDispatchTime, now)
                .set(Orders::getDispatchRiderId, bestRiderId)
                .set(Orders::getOldRiderId, bestRiderId));

        if (updated == 0) {
            log.debug("订单 {} 派单 CAS 失败(可能已被抢单)", order.getId());
            return null;
        }

        log.info("订单 {} 自动派单给骑手 {},评分={}", order.getId(), bestRiderId, scores.get(0).score);

        // 通知接入方
        notifyOrderEvent(order.getId(), "order.dispatched");

        // 清除拒单记录
        refuseMapper.delete(new LambdaQueryWrapper<RiderOrderRefuse>()
                .eq(RiderOrderRefuse::getOid, order.getId()));

        return bestRiderId;
    }

    /**
     * 评估单个条件,返回得分
     * @return 得分:1.0=完全匹配, 0~1=部分匹配, -1=强制不通过
     */
    private double evaluateCondition(DispatchRuleTemplateVO.ConditionItem cond,
                                     Rider rider, RiderLocation loc, Orders order,
                                     double distanceToPickup,
                                     double riderLat, double riderLng,
                                     double orderLat, double orderLng,
                                     double orderTLat, double orderTLng,
                                     int currentLoad, int dailyCount) {
        DispatchConditionType type = DispatchConditionType.fromCode(cond.getConditionType());
        if (type == null) return 0;

        double threshold = cond.getThresholdValue() != null ? cond.getThresholdValue().doubleValue() : 0;

        return switch (type) {
            case DISTANCE -> {
                // 距离取单地址 ≤ N 公里,超过则过滤
                if (threshold > 0 && distanceToPickup > threshold) yield -1;
                // 距离越近分越高(归一化到 0~1)
                yield threshold > 0 ? Math.max(0, 1.0 - distanceToPickup / threshold) : 1.0;
            }
            case DETOUR -> {
                // 顺路距离:骑手当前有在途订单时,计算绕路程度
                // 简化实现:以骑手到取货点的距离作为近似
                if (threshold > 0 && distanceToPickup > threshold) yield -1;
                yield threshold > 0 ? Math.max(0, 1.0 - distanceToPickup / threshold) : 1.0;
            }
            case WAIT -> {
                // 等待新订单时间 ≥ N 分钟:骑手最后完成订单距今
                long lastCompleteTime = getLastCompleteTime(rider.getId());
                long now = System.currentTimeMillis() / 1000;
                double waitMinutes = lastCompleteTime > 0 ? (now - lastCompleteTime) / 60.0 : 999;
                yield waitMinutes >= threshold ? 1.0 : waitMinutes / threshold;
            }
            case CURRENT_LOAD -> {
                // 当前持单量 < N
                if (threshold > 0 && currentLoad >= threshold) yield -1;
                yield threshold > 0 ? Math.max(0, 1.0 - currentLoad / threshold) : 1.0;
            }
            case DAILY -> {
                // 当日总接单量 < N
                if (threshold > 0 && dailyCount >= threshold) yield -1;
                yield threshold > 0 ? Math.max(0, 1.0 - dailyCount / threshold) : 1.0;
            }
            case DIRECTION -> {
                // 目的地方向一致性:骑手位置到订单取货点的方向 vs 取货点到收货点的方向
                double bearingToPickup = calcBearing(riderLat, riderLng, orderLat, orderLng);
                double bearingPickupToDest = calcBearing(orderLat, orderLng, orderTLat, orderTLng);
                double diff = Math.abs(bearingToPickup - bearingPickupToDest);
                if (diff > 180) diff = 360 - diff;
                // 方向差 ≤ 45度算顺路
                yield diff <= 45 ? 1.0 : (diff <= 90 ? 0.5 : 0);
            }
            case ROOKIE -> {
                // 新手保护:入职 ≤ N 天的骑手优先
                long createTime = rider.getCreateTime() != null ? rider.getCreateTime() : 0;
                long now = System.currentTimeMillis() / 1000;
                double daysRegistered = (now - createTime) / 86400.0;
                yield daysRegistered <= threshold ? 1.0 : 0;
            }
            case PRAISE -> {
                // 好评率 ≥ N%
                int starTotal = rider.getStarTotal() != null ? rider.getStarTotal() : 0;
                int starCount = rider.getStarCount() != null ? rider.getStarCount() : 0;
                if (starCount == 0) yield 0.5; // 无评价的骑手给中间分
                double avgStar = (double) starTotal / starCount;
                double praiseRate = avgStar / 5.0 * 100; // 转换为百分比
                yield praiseRate >= threshold ? 1.0 : praiseRate / threshold;
            }
            case AREA_MATCH -> {
                // 区域匹配:骑手当前所在城市与订单城市匹配
                // 目前按 city_id 匹配(候选骑手已按 cityId 过滤,所以这里总是匹配的)
                // 后续可扩展为更细粒度的区域
                yield 1.0;
            }
        };
    }

    // ---- 辅助方法 ----

    private List<Long> getRefusedRiderIds(Long orderId) {
        List<RiderOrderRefuse> refuses = refuseMapper.selectList(
                new LambdaQueryWrapper<RiderOrderRefuse>()
                        .eq(RiderOrderRefuse::getOid, orderId)
                        .select(RiderOrderRefuse::getRiderId));
        return refuses.stream().map(RiderOrderRefuse::getRiderId).toList();
    }

    private Map<Long, RiderLocation> getLocationMap(List<Long> riderIds) {
        if (riderIds.isEmpty()) return Map.of();
        List<RiderLocation> locations = locationMapper.selectList(
                new LambdaQueryWrapper<RiderLocation>().in(RiderLocation::getUid, riderIds));
        Map<Long, RiderLocation> map = new HashMap<>();
        for (RiderLocation loc : locations) {
            map.put(loc.getUid(), loc);
        }
        return map;
    }

    /** 获取每个骑手当前持单量(status=3 或 4 的订单数) */
    private Map<Long, Integer> getCurrentLoadMap(List<Long> riderIds) {
        Map<Long, Integer> map = new HashMap<>();
        if (riderIds.isEmpty()) return map;
        List<Orders> inProgress = ordersMapper.selectList(
                new LambdaQueryWrapper<Orders>()
                        .in(Orders::getRiderId, riderIds)
                        .in(Orders::getStatus, List.of(3, 4))
                        .select(Orders::getRiderId));
        for (Orders o : inProgress) {
            map.merge(o.getRiderId(), 1, Integer::sum);
        }
        return map;
    }

    /** 获取每个骑手当日接单量 */
    private Map<Long, Integer> getDailyCountMap(List<Long> riderIds) {
        Map<Long, Integer> map = new HashMap<>();
        if (riderIds.isEmpty()) return map;
        long todayStart = LocalDate.now().atStartOfDay(ZoneId.of("Asia/Shanghai")).toEpochSecond();
        int countDate = Integer.parseInt(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
        List<RiderOrderCount> counts = countMapper.selectList(
                new LambdaQueryWrapper<RiderOrderCount>()
                        .in(RiderOrderCount::getUid, riderIds)
                        .eq(RiderOrderCount::getCountDate, countDate));
        for (RiderOrderCount c : counts) {
            map.put(c.getUid(), c.getOrders());
        }
        return map;
    }

    /** 获取骑手最后完成订单的时间 */
    private long getLastCompleteTime(Long riderId) {
        Orders last = ordersMapper.selectOne(
                new LambdaQueryWrapper<Orders>()
                        .eq(Orders::getRiderId, riderId)
                        .eq(Orders::getStatus, 6)
                        .orderByDesc(Orders::getCompleteTime)
                        .select(Orders::getCompleteTime)
                        .last("LIMIT 1"));
        return last != null && last.getCompleteTime() != null ? last.getCompleteTime() : 0;
    }

    /** 计算两点间的方位角(度数,0=北 90=东) */
    private double calcBearing(double lat1, double lng1, double lat2, double lng2) {
        double dLng = Math.toRadians(lng2 - lng1);
        double lat1Rad = Math.toRadians(lat1);
        double lat2Rad = Math.toRadians(lat2);
        double y = Math.sin(dLng) * Math.cos(lat2Rad);
        double x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng);
        double bearing = Math.toDegrees(Math.atan2(y, x));
        return (bearing + 360) % 360;
    }

    private double parseDouble(String s) {
        try {
            return s != null ? Double.parseDouble(s) : 0;
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    private void notifyOrderEvent(Long orderId, String event) {
        try {
            Orders order = ordersMapper.selectById(orderId);
            if (order == null || order.getAppKey() == null || order.getAppKey().isBlank()) return;
            Map<String, Object> payload = new HashMap<>();
            payload.put("event", event);
            payload.put("outOrderNo", order.getOutOrderNo());
            payload.put("deliveryOrderId", order.getId());
            payload.put("orderNo", order.getOrderNo());
            payload.put("status", order.getStatus());
            payload.put("riderId", order.getRiderId());
            payload.put("dispatchRiderId", order.getDispatchRiderId());
            payload.put("timestamp", System.currentTimeMillis() / 1000);
            webhookService.send(event, orderId, objectMapper.writeValueAsString(payload));
        } catch (Exception e) {
            log.warn("派单事件通知失败 orderId={} event={}", orderId, event, e);
        }
    }

    private record RiderScore(Long riderId, double score, double distance) {}
}