|
|
@@ -0,0 +1,349 @@
|
|
|
+package com.kxmall.web.controller.rider.service.impl;
|
|
|
+
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.kxmall.common.enums.RiderStatusType;
|
|
|
+import com.kxmall.common.enums.RiderWorkStateType;
|
|
|
+import com.kxmall.rider.domain.KxRider;
|
|
|
+import com.kxmall.rider.domain.KxRiderExt;
|
|
|
+import com.kxmall.rider.domain.vo.KxRiderVo;
|
|
|
+import com.kxmall.rider.mapper.KxRiderExtMapper;
|
|
|
+import com.kxmall.rider.mapper.KxRiderMapper;
|
|
|
+import com.kxmall.web.controller.rider.config.SmartDispatchConfig;
|
|
|
+import com.kxmall.order.biz.bo.SmartDispatchParamBo;
|
|
|
+import com.kxmall.web.controller.rider.service.ISmartDispatchService;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.text.ParseException;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 智能派单服务实现类
|
|
|
+ * 基于服务类型和距离的师傅推荐算法
|
|
|
+ *
|
|
|
+ * @author kxmall
|
|
|
+ * @date 2025-01-08
|
|
|
+ */
|
|
|
+@Service
|
|
|
+@Slf4j
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class SmartDispatchServiceImpl implements ISmartDispatchService {
|
|
|
+
|
|
|
+ private final KxRiderMapper riderMapper;
|
|
|
+ private final KxRiderExtMapper riderExtMapper;
|
|
|
+ private final SmartDispatchConfig smartDispatchConfig;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<KxRiderVo> recommendRiders(SmartDispatchParamBo dispatchParam) {
|
|
|
+ // 获取可用的师傅列表
|
|
|
+ List<KxRiderVo> availableRiders = getAvailableRiders();
|
|
|
+
|
|
|
+ if (CollUtil.isEmpty(availableRiders)) {
|
|
|
+ log.warn("没有可用的师傅,storageId: {}", dispatchParam.getStorageId());
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算匹配度并排序
|
|
|
+ List<KxRiderVo> rankedRiders = availableRiders.stream()
|
|
|
+ .peek(rider -> {
|
|
|
+ Double matchScore = calculateMatchScore(rider, dispatchParam);
|
|
|
+ rider.setMatchScore(matchScore);
|
|
|
+ })
|
|
|
+ .sorted((r1, r2) -> Double.compare(r2.getMatchScore(), r1.getMatchScore()))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ log.info("为订单 {} 推荐了 {} 个师傅,最佳匹配度: {}",
|
|
|
+ dispatchParam.getOrderNo(), rankedRiders.size(),
|
|
|
+ rankedRiders.isEmpty() ? 0 : rankedRiders.get(0).getMatchScore());
|
|
|
+
|
|
|
+ return rankedRiders;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public KxRiderVo selectBestRider(SmartDispatchParamBo dispatchParam) {
|
|
|
+ List<KxRiderVo> recommendedRiders = recommendRiders(dispatchParam);
|
|
|
+
|
|
|
+ if (CollUtil.isEmpty(recommendedRiders)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ KxRiderVo bestRider = recommendedRiders.get(0);
|
|
|
+
|
|
|
+ // 根据紧急程度调整阈值
|
|
|
+ double threshold = getMatchThreshold(dispatchParam.getUrgencyLevel());
|
|
|
+
|
|
|
+ if (bestRider.getMatchScore() >= threshold) {
|
|
|
+ log.info("自动选择师傅: {} (ID: {}), 匹配度: {}, 阈值: {}",
|
|
|
+ bestRider.getName(), bestRider.getId(), bestRider.getMatchScore(), threshold);
|
|
|
+ return bestRider;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.warn("最佳师傅匹配度不足{}分: {}, 不进行自动派单", threshold, bestRider.getMatchScore());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Double calculateMatchScore(KxRiderVo rider, SmartDispatchParamBo dispatchParam) {
|
|
|
+ try {
|
|
|
+ double serviceTypeScore = calculateServiceTypeScore(rider, dispatchParam);
|
|
|
+ double distanceScore = calculateDistanceScore(rider, dispatchParam);
|
|
|
+ double ratingScore = calculateRatingScore(rider);
|
|
|
+ double experienceScore = calculateExperienceScore(rider);
|
|
|
+ double urgencyMultiplier = getUrgencyMultiplier(dispatchParam.getUrgencyLevel());
|
|
|
+
|
|
|
+ // 加权综合评分
|
|
|
+ double totalScore = (serviceTypeScore * smartDispatchConfig.getServiceTypeWeight()
|
|
|
+ + distanceScore * smartDispatchConfig.getDistanceWeight()
|
|
|
+ + ratingScore * smartDispatchConfig.getRatingWeight()
|
|
|
+ + experienceScore * smartDispatchConfig.getMinMatchScore()) * urgencyMultiplier;
|
|
|
+
|
|
|
+ log.debug("师傅 {} 匹配度: 服务={}, 距离={}, 评分={}, 经验={}, 紧急倍数={}, 总分={}",
|
|
|
+ rider.getName(), serviceTypeScore, distanceScore, ratingScore, experienceScore, urgencyMultiplier, totalScore);
|
|
|
+
|
|
|
+ return Math.round(totalScore * 100.0) / 100.0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("计算师傅匹配度时发生异常: {}", e.getMessage(), e);
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算服务类型匹配度
|
|
|
+ */
|
|
|
+ private double calculateServiceTypeScore(KxRiderVo rider, SmartDispatchParamBo dispatchParam) {
|
|
|
+ String workTypeKeywords = rider.getWorkTypeKeywords();
|
|
|
+
|
|
|
+ if (StrUtil.isBlank(workTypeKeywords)) {
|
|
|
+ return 50.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<String> serviceKeywords = dispatchParam.getServiceKeywords();
|
|
|
+ if (CollUtil.isEmpty(serviceKeywords)) {
|
|
|
+ return 60.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> riderKeywords = Arrays.stream(workTypeKeywords.split(","))
|
|
|
+ .filter(StrUtil::isNotBlank)
|
|
|
+ .map(String::trim)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+
|
|
|
+ if (riderKeywords.isEmpty()) {
|
|
|
+ return 50.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ long matchCount = serviceKeywords.stream()
|
|
|
+ .mapToLong(serviceKeyword ->
|
|
|
+ riderKeywords.stream()
|
|
|
+ .anyMatch(riderKeyword ->
|
|
|
+ riderKeyword.contains(serviceKeyword.toLowerCase()) ||
|
|
|
+ serviceKeyword.contains(riderKeyword)) ? 1 : 0)
|
|
|
+ .sum();
|
|
|
+
|
|
|
+ if (matchCount == 0) {
|
|
|
+ return 30.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ double matchRatio = (double) matchCount / serviceKeywords.size();
|
|
|
+ return Math.min(100.0, 40.0 + matchRatio * 60.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据紧急程度获取匹配度阈值
|
|
|
+ */
|
|
|
+ private double getMatchThreshold(Integer urgencyLevel) {
|
|
|
+ if (urgencyLevel == null) {
|
|
|
+ return 60.0; // 默认阈值
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (urgencyLevel) {
|
|
|
+ case 1: // 普通
|
|
|
+ return 60.0;
|
|
|
+ case 2: // 紧急
|
|
|
+ return 50.0;
|
|
|
+ case 3: // 特急
|
|
|
+ return 40.0;
|
|
|
+ default:
|
|
|
+ return 60.0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据紧急程度获取评分倍数
|
|
|
+ */
|
|
|
+ private double getUrgencyMultiplier(Integer urgencyLevel) {
|
|
|
+ if (urgencyLevel == null) {
|
|
|
+ return 1.0; // 默认倍数
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (urgencyLevel) {
|
|
|
+ case 1: // 普通
|
|
|
+ return 1.0;
|
|
|
+ case 2: // 紧急
|
|
|
+ return 1.1;
|
|
|
+ case 3: // 特急
|
|
|
+ return 1.2;
|
|
|
+ default:
|
|
|
+ return 1.0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算距离评分
|
|
|
+ */
|
|
|
+ private double calculateDistanceScore(KxRiderVo rider, SmartDispatchParamBo dispatchParam) {
|
|
|
+ BigDecimal workRadio = rider.getWorkRadio();
|
|
|
+
|
|
|
+ if (workRadio == null || workRadio.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return 70.0; // 没有设置工作半径,给中等分数
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dispatchParam.getLatitude() == null || dispatchParam.getLongitude() == null) {
|
|
|
+ return 70.0; // 没有订单位置信息
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rider.getLatitude() == null || rider.getLongitude() == null) {
|
|
|
+ return 70.0; // 没有师傅位置信息
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算实际距离 (单位: 公里)
|
|
|
+ double distance = calculateDistance(
|
|
|
+ rider.getLatitude().doubleValue(), rider.getLongitude().doubleValue(),
|
|
|
+ dispatchParam.getLatitude().doubleValue(), dispatchParam.getLongitude().doubleValue()
|
|
|
+ );
|
|
|
+
|
|
|
+ double workRadioKm = workRadio.doubleValue();
|
|
|
+
|
|
|
+ if (distance <= workRadioKm * 0.3) {
|
|
|
+ return 100.0; // 在工作半径30%内,满分
|
|
|
+ } else if (distance <= workRadioKm * 0.6) {
|
|
|
+ return 85.0; // 在工作半径60%内,高分
|
|
|
+ } else if (distance <= workRadioKm) {
|
|
|
+ return 70.0; // 在工作半径内,中等分数
|
|
|
+ } else if (distance <= workRadioKm * 1.2) {
|
|
|
+ return 50.0; // 稍微超出工作半径,低分
|
|
|
+ } else {
|
|
|
+ return 20.0; // 远超工作半径,很低分
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算师傅评分
|
|
|
+ */
|
|
|
+ private double calculateRatingScore(KxRiderVo rider) {
|
|
|
+ // 获取师傅扩展信息
|
|
|
+ KxRiderExt riderExt = riderExtMapper.selectById(rider.getId());
|
|
|
+ BigDecimal rating = riderExt.getCurrentTotalRating().divide(BigDecimal.valueOf(riderExt.getCurrentTotalReviews()), 2, RoundingMode.HALF_UP);
|
|
|
+
|
|
|
+ if (rating.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return 60.0; // 没有评分
|
|
|
+ }
|
|
|
+
|
|
|
+ // 假设评分是5分制,转换为100分制
|
|
|
+ double score = rating.doubleValue() * 20; // 5分 -> 100分
|
|
|
+ return Math.min(100.0, Math.max(0.0, score));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算经验评分
|
|
|
+ */
|
|
|
+ private double calculateExperienceScore(KxRiderVo rider) {
|
|
|
+ // 获取师傅扩展信息
|
|
|
+ KxRiderExt riderExt = riderExtMapper.selectById(rider.getId());
|
|
|
+
|
|
|
+ int orderCount = riderExt.getCurrentTotalOrderCount();
|
|
|
+
|
|
|
+ if (orderCount >= 500) {
|
|
|
+ return 100.0; // 500单以上,满分
|
|
|
+ } else if (orderCount >= 400) {
|
|
|
+ return 85.0; // 200-499单,高分
|
|
|
+ } else if (orderCount >= 300) {
|
|
|
+ return 70.0; // 100-199单,中高分
|
|
|
+ } else if (orderCount >= 200) {
|
|
|
+ return 55.0; // 50-99单,中等分
|
|
|
+ } else if (orderCount >= 100) {
|
|
|
+ return 40.0; // 10-49单,中低分
|
|
|
+ } else {
|
|
|
+ return 25.0; // 10单以下,新手分数
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取可用的师傅列表
|
|
|
+ */
|
|
|
+ private List<KxRiderVo> getAvailableRiders() {
|
|
|
+ // 获取当前时间和星期
|
|
|
+ Date now = new Date();
|
|
|
+ SimpleDateFormat format = new SimpleDateFormat("HH:mm");
|
|
|
+ String nowTime = format.format(now);
|
|
|
+
|
|
|
+ // 查询在工作时间范围内的师傅
|
|
|
+ int state = RiderStatusType.NOMRAL.getCode();
|
|
|
+ int workState = RiderWorkStateType.WORKING.getCode();
|
|
|
+
|
|
|
+ LambdaQueryWrapper<KxRider> lqw = new LambdaQueryWrapper<>();
|
|
|
+ lqw.eq(KxRider::getState, state)
|
|
|
+ .eq(KxRider::getWorkState, workState);
|
|
|
+ List<KxRider> riderList = riderMapper.selectList(lqw);
|
|
|
+
|
|
|
+ if (CollUtil.isEmpty(riderList)) {
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 过滤在当前时间可工作的师傅并转换为VO
|
|
|
+ return riderList.stream()
|
|
|
+ .filter(rider -> {
|
|
|
+ try {
|
|
|
+ return isEffectiveDate(nowTime, rider.getDeliveryStart(), rider.getDeliveryEnd());
|
|
|
+ } catch (ParseException e) {
|
|
|
+ log.warn("解析师傅{}工作时间失败: {}", rider.getName(), e.getMessage());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .map(rider -> {
|
|
|
+ KxRiderVo riderVo = new KxRiderVo();
|
|
|
+ BeanUtils.copyProperties(rider, riderVo);
|
|
|
+ return riderVo;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算两点之间的距离 (公里)
|
|
|
+ */
|
|
|
+ private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
|
|
+ final int R = 6371; // 地球半径,公里
|
|
|
+
|
|
|
+ double latDistance = Math.toRadians(lat2 - lat1);
|
|
|
+ double lonDistance = Math.toRadians(lon2 - lon1);
|
|
|
+ double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
|
|
|
+ + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
|
|
|
+ * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
|
|
|
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
+
|
|
|
+ return R * c; // 返回公里
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断当前时间是否在工作时间区间内
|
|
|
+ */
|
|
|
+ private boolean isEffectiveDate(String nowDate, String startDate, String endDate) throws ParseException {
|
|
|
+ String format = "HH:mm";
|
|
|
+ if (StrUtil.isBlank(startDate) || StrUtil.isBlank(endDate)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Date startTime = new SimpleDateFormat(format).parse(startDate);
|
|
|
+ Date endTime = new SimpleDateFormat(format).parse(endDate);
|
|
|
+ Date currentTime = new SimpleDateFormat(format).parse(nowDate);
|
|
|
+
|
|
|
+ return currentTime.getTime() >= startTime.getTime() && currentTime.getTime() <= endTime.getTime();
|
|
|
+ }
|
|
|
+}
|