Browse Source

add ws&智能派单

tea 3 months ago
parent
commit
43ecfa9d6b
46 changed files with 2036 additions and 146 deletions
  1. 25 0
      kxmall-admin-api/src/main/java/com/kxmall/web/controller/quartz/OrderQuartz.java
  2. 12 0
      kxmall-admin-api/src/main/java/com/kxmall/web/controller/quartz/service/OrderQuartzService.java
  3. 74 0
      kxmall-admin-api/src/main/java/com/kxmall/web/controller/quartz/service/impl/OrderQuartzServiceImpl.java
  4. 56 0
      kxmall-admin-api/src/main/java/com/kxmall/web/controller/rider/config/SmartDispatchConfig.java
  5. 40 0
      kxmall-admin-api/src/main/java/com/kxmall/web/controller/rider/service/ISmartDispatchService.java
  6. 20 8
      kxmall-admin-api/src/main/java/com/kxmall/web/controller/rider/service/impl/KxRiderServiceImpl.java
  7. 349 0
      kxmall-admin-api/src/main/java/com/kxmall/web/controller/rider/service/impl/SmartDispatchServiceImpl.java
  8. 19 2
      kxmall-app-api/src/main/java/com/kxmall/web/controller/callback/CallbackController.java
  9. 5 0
      kxmall-app-api/src/main/java/com/kxmall/web/controller/order/builder/OrderBuilder.java
  10. 12 0
      kxmall-app-api/src/main/java/com/kxmall/web/controller/order/builder/OrderConcreteBuilder.java
  11. 1 0
      kxmall-app-api/src/main/java/com/kxmall/web/controller/order/builder/OrderDirector.java
  12. 2 2
      kxmall-app-api/src/main/java/com/kxmall/web/controller/quartz/CheckQuartz.java
  13. 2 0
      kxmall-common/src/main/java/com/kxmall/common/constant/CacheConstants.java
  14. 19 0
      kxmall-common/src/main/java/com/kxmall/common/enums/BooleanE.java
  15. 0 24
      kxmall-common/src/main/java/com/kxmall/common/enums/RiderWeekStatusType.java
  16. 10 12
      kxmall-common/src/main/java/com/kxmall/common/enums/RiderWorkStateType.java
  17. 10 0
      kxmall-common/src/main/java/com/kxmall/common/exception/KxRuntimeException.java
  18. 37 0
      kxmall-common/src/main/java/com/kxmall/common/service/RiderNotificationService.java
  19. 12 8
      kxmall-common/src/main/java/com/kxmall/common/utils/redis/RedisUtils.java
  20. 6 0
      kxmall-framework/pom.xml
  21. 24 0
      kxmall-framework/src/main/java/com/kxmall/framework/config/WebSocketConfig.java
  22. 1 1
      kxmall-generator/src/test/java/Gen.java
  23. 15 7
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/quartz/CheckRiderQuartz.java
  24. 12 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/quartz/service/RiderQuartzService.java
  25. 76 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/quartz/service/impl/RiderQuartzServiceImpl.java
  26. 13 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/task/service/impl/TaskCenterServiceImpl.java
  27. 103 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/RiderWebSocketController.java
  28. 187 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/RiderWebSocketServer.java
  29. 119 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/dto/OrderNotificationDTO.java
  30. 62 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/service/RiderNotificationService.java
  31. 114 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/service/impl/CommonRiderNotificationServiceImpl.java
  32. 147 0
      kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/service/impl/RiderNotificationServiceImpl.java
  33. 68 73
      kxmall-system/src/main/java/com/kxmall/order/biz/OrderBizService.java
  34. 4 7
      kxmall-system/src/main/java/com/kxmall/order/biz/OrderRiderBizService.java
  35. 71 0
      kxmall-system/src/main/java/com/kxmall/order/biz/bo/SmartDispatchParamBo.java
  36. 31 0
      kxmall-system/src/main/java/com/kxmall/order/biz/bo/package-info.java
  37. 46 0
      kxmall-system/src/main/java/com/kxmall/rider/domain/KxRiderExt.java
  38. 2 0
      kxmall-system/src/main/java/com/kxmall/rider/domain/vo/KxRiderVo.java
  39. 21 0
      kxmall-system/src/main/java/com/kxmall/rider/mapper/KxRiderExtMapper.java
  40. 1 1
      kxmall-system/src/main/java/com/kxmall/rider/mapper/KxRiderMapper.java
  41. 4 0
      kxmall-system/src/main/java/com/kxmall/rider/mapper/KxRiderOrderMapper.java
  42. 17 0
      kxmall-system/src/main/resources/mapper/rider/KxRiderExtMapper.xml
  43. 19 0
      kxmall-system/src/main/resources/mapper/rider/KxRiderMapper.xml
  44. 4 0
      kxmall-system/src/main/resources/mapper/rider/KxRiderOrderMapper.xml
  45. 163 0
      智能派单算法说明.md
  46. 1 1
      需求列表.md

+ 25 - 0
kxmall-admin-api/src/main/java/com/kxmall/web/controller/quartz/OrderQuartz.java

@@ -0,0 +1,25 @@
+package com.kxmall.web.controller.quartz;
+
+import com.kxmall.web.controller.quartz.service.OrderQuartzService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * @author tea
+ * @date 2025/9/7
+ */
+@Component
+@EnableScheduling
+@RequiredArgsConstructor
+public class OrderQuartz {
+
+    private final OrderQuartzService orderQuartzService;
+
+    @Scheduled(fixedRate = 60 * 1000)
+    public void smartDispatch() {
+        orderQuartzService.smartDispatch();
+    }
+}

+ 12 - 0
kxmall-admin-api/src/main/java/com/kxmall/web/controller/quartz/service/OrderQuartzService.java

@@ -0,0 +1,12 @@
+package com.kxmall.web.controller.quartz.service;
+
+/**
+ *
+ * @author tea
+ * @date 2025/9/7
+ */
+public interface OrderQuartzService {
+
+    void smartDispatch();
+
+}

+ 74 - 0
kxmall-admin-api/src/main/java/com/kxmall/web/controller/quartz/service/impl/OrderQuartzServiceImpl.java

@@ -0,0 +1,74 @@
+package com.kxmall.web.controller.quartz.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import com.kxmall.common.constant.CacheConstants;
+import com.kxmall.common.service.RiderNotificationService;
+import com.kxmall.common.utils.redis.RedisUtils;
+import com.kxmall.order.biz.bo.SmartDispatchParamBo;
+import com.kxmall.rider.domain.vo.KxRiderVo;
+import com.kxmall.web.controller.quartz.service.OrderQuartzService;
+import com.kxmall.web.controller.rider.service.ISmartDispatchService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @author tea
+ * @date 2025/9/7
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class OrderQuartzServiceImpl implements OrderQuartzService {
+
+    private final ISmartDispatchService smartDispatchService;
+    private final RiderNotificationService riderNotificationService;
+
+    @Override
+    public void smartDispatch() {
+        Map<String, SmartDispatchParamBo> waitDispatchOrderMap = RedisUtils.getCacheMap(CacheConstants.WAIT_DISPATCH_ORDERS);
+
+        if (MapUtil.isNotEmpty(waitDispatchOrderMap)) {
+            waitDispatchOrderMap.values().parallelStream().forEach(this::processOrderDispatch);
+        }
+    }
+
+    /**
+     * 处理单个订单的智能派单
+     */
+    private void processOrderDispatch(SmartDispatchParamBo smartDispatchParamBo) {
+        try {
+            // 获取推荐骑手列表
+            List<KxRiderVo> recommendedRiders = smartDispatchService.recommendRiders(smartDispatchParamBo);
+
+            if (CollUtil.isEmpty(recommendedRiders)) {
+                log.warn("订单{}没有找到合适的骑手", smartDispatchParamBo.getOrderNo());
+                return;
+            }
+
+            List<String> riderIds = recommendedRiders.stream()
+                    .map(rider -> String.valueOf(rider.getId()))
+                    .collect(Collectors.toList());
+
+            List<Double> matchScores = recommendedRiders.stream()
+                    .map(KxRiderVo::getMatchScore)
+                    .collect(Collectors.toList());
+
+            riderNotificationService.sendNewOrderNotification(
+                    smartDispatchParamBo, riderIds, matchScores);
+
+            log.info("订单{}智能派单完成,推荐{}个骑手",
+                    smartDispatchParamBo.getOrderNo(), recommendedRiders.size());
+
+        } catch (Exception e) {
+            log.error("处理订单{}智能派单失败", smartDispatchParamBo.getOrderNo(), e);
+        }
+    }
+}

+ 56 - 0
kxmall-admin-api/src/main/java/com/kxmall/web/controller/rider/config/SmartDispatchConfig.java

@@ -0,0 +1,56 @@
+package com.kxmall.web.controller.rider.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 智能派单配置
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+@Component
+@ConfigurationProperties(prefix = "kxmall.smart-dispatch")
+@Getter
+@Setter
+public class SmartDispatchConfig {
+
+    /**
+     * 是否启用智能派单
+     */
+    private boolean enabled = true;
+
+    /**
+     * 服务类型匹配权重 (0.0-1.0)
+     */
+    private double serviceTypeWeight = 0.4;
+
+    /**
+     * 距离权重 (0.0-1.0)
+     */
+    private double distanceWeight = 0.3;
+
+    /**
+     * 评分权重 (0.0-1.0)
+     */
+    private double ratingWeight = 0.2;
+
+    /**
+     * 经验权重 (0.0-1.0)
+     */
+    private double experienceWeight = 0.1;
+
+    /**
+     * 最低匹配度阈值,低于此分数不推荐
+     */
+    private double minMatchScore = 60.0;
+
+    /**
+     * 最大推荐师傅数量
+     */
+    private int maxRecommendCount = 10;
+
+    private boolean onlyFirst = false;
+}

+ 40 - 0
kxmall-admin-api/src/main/java/com/kxmall/web/controller/rider/service/ISmartDispatchService.java

@@ -0,0 +1,40 @@
+package com.kxmall.web.controller.rider.service;
+
+import com.kxmall.rider.domain.vo.KxRiderVo;
+import com.kxmall.order.biz.bo.SmartDispatchParamBo;
+
+import java.util.List;
+
+/**
+ * 智能派单服务接口
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+public interface ISmartDispatchService {
+
+    /**
+     * 基于服务类型和距离智能推荐最适合的师傅
+     * 
+     * @param dispatchParam 智能派单参数
+     * @return 推荐的师傅列表,按推荐度排序
+     */
+    List<KxRiderVo> recommendRiders(SmartDispatchParamBo dispatchParam);
+
+    /**
+     * 自动选择最佳师傅
+     * 
+     * @param dispatchParam 智能派单参数
+     * @return 最佳师傅,如果没有合适的返回null
+     */
+    KxRiderVo selectBestRider(SmartDispatchParamBo dispatchParam);
+
+    /**
+     * 计算师傅与订单的匹配度
+     * 
+     * @param rider 师傅信息
+     * @param dispatchParam 派单参数信息
+     * @return 匹配度分数 (0-100)
+     */
+    Double calculateMatchScore(KxRiderVo rider, SmartDispatchParamBo dispatchParam);
+}

+ 20 - 8
kxmall-admin-api/src/main/java/com/kxmall/web/controller/rider/service/impl/KxRiderServiceImpl.java

@@ -9,23 +9,24 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.kxmall.common.core.domain.PageQuery;
 import com.kxmall.common.core.page.TableDataInfo;
 import com.kxmall.common.enums.RiderStatusType;
-import com.kxmall.common.enums.RiderWeekStatusType;
 import com.kxmall.common.enums.RiderWorkStateType;
 import com.kxmall.common.exception.ServiceException;
 import com.kxmall.common.utils.StringUtils;
 import com.kxmall.common.utils.file.FileUtils;
 import com.kxmall.rider.domain.KxRider;
+import com.kxmall.rider.domain.KxRiderAuthAttachment;
 import com.kxmall.rider.domain.KxRiderCycle;
+import com.kxmall.rider.domain.KxRiderExt;
 import com.kxmall.rider.domain.bo.KxRiderBo;
 import com.kxmall.rider.domain.vo.KxRiderVo;
 import com.kxmall.rider.mapper.KxRiderCycleMapper;
+import com.kxmall.rider.mapper.KxRiderExtMapper;
 import com.kxmall.rider.mapper.KxRiderMapper;
-import com.kxmall.rider.domain.KxRiderAuthAttachment;
 import com.kxmall.storage.domain.KxStorage;
 import com.kxmall.storage.mapper.KxStorageMapper;
 import com.kxmall.system.service.ISysConfigService;
-import com.kxmall.web.controller.rider.service.IKxRiderService;
 import com.kxmall.web.controller.rider.service.IKxRiderAuthAttachmentService;
+import com.kxmall.web.controller.rider.service.IKxRiderService;
 import com.kxmall.wechat.WxMpConfiguration;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -38,6 +39,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.ObjectUtils;
 
 import java.io.File;
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
@@ -64,6 +66,8 @@ public class KxRiderServiceImpl implements IKxRiderService {
 
     private final IKxRiderAuthAttachmentService attachmentService;
 
+    private final KxRiderExtMapper riderExtMapper;
+
     // 初始密码
     private static final String ININT_PASSWORD = "123456";
 
@@ -137,6 +141,7 @@ public class KxRiderServiceImpl implements IKxRiderService {
      * 新增配送
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Boolean insertByBo(KxRiderBo bo) {
         if (StringUtils.isEmpty(bo.getPhone())) {
             throw new ServiceException("配送员手机号不能为空");
@@ -157,7 +162,7 @@ public class KxRiderServiceImpl implements IKxRiderService {
         String cryptPassword = Md5Crypt.md5Crypt(add.getPassword().getBytes(), "$1$" + bo.getPhone().substring(0, 7));
         add.setPassword(cryptPassword);
         add.setState(RiderStatusType.NOMRAL.getCode());
-        add.setWorkState(RiderWeekStatusType.REST.getCode());
+        add.setWorkState(RiderWorkStateType.WORKING.getCode());
         add.setCreateBy(bo.getUserName());
         add.setUpdateBy(bo.getUserName());
         Date now = new Date();
@@ -185,6 +190,13 @@ public class KxRiderServiceImpl implements IKxRiderService {
                 riderCycleMapper.insertBatch(riderCycleDOList);
             }
             add.setPassword(null);
+
+            KxRiderExt riderExt = new KxRiderExt();
+            riderExt.setRiderId(riderDOId);
+            riderExt.setCurrentTotalOrderCount(0);
+            riderExt.setCurrentTotalReviews(0);
+            riderExt.setCurrentTotalRating(BigDecimal.ZERO);
+            riderExtMapper.insert(riderExt);
             return true;
         }
         throw new ServiceException("管理员系统未知异常");
@@ -271,9 +283,9 @@ public class KxRiderServiceImpl implements IKxRiderService {
         }
         KxStorage kxStorage = storageMapper.selectById(storageId);
         wrapper.eq("state", RiderStatusType.NOMRAL.getCode());
-        wrapper.eq("work_state", RiderWeekStatusType.BUSINESS.getCode());
+        wrapper.eq("work_state", RiderWorkStateType.WORKING.getCode());
         int state = RiderStatusType.NOMRAL.getCode();
-        int workState = RiderWeekStatusType.BUSINESS.getCode();
+        int workState = RiderWorkStateType.WORKING.getCode();
         //过滤不在工作区间
         Date now = new Date();
         SimpleDateFormat format = new SimpleDateFormat("HH:mm");
@@ -392,7 +404,7 @@ public class KxRiderServiceImpl implements IKxRiderService {
         if (CollectionUtils.isEmpty(ids)) {
             throw new ServiceException("配送员不存在");
         }
-        if (baseMapper.batchUpdateWeekState(ids, RiderWeekStatusType.BUSINESS.getCode()) <= 0) {
+        if (baseMapper.batchUpdateWeekState(ids, RiderWorkStateType.WORKING.getCode()) <= 0) {
             throw new ServiceException("配送员不存在");
         }
         return "ok";
@@ -404,7 +416,7 @@ public class KxRiderServiceImpl implements IKxRiderService {
         if (CollectionUtils.isEmpty(ids)) {
             throw new ServiceException("配送员不存在");
         }
-        if (baseMapper.batchUpdateWeekState(ids, RiderWeekStatusType.REST.getCode()) <= 0) {
+        if (baseMapper.batchUpdateWeekState(ids, RiderWorkStateType.WORKING.getCode()) <= 0) {
             throw new ServiceException("配送员不存在");
         }
         return "ok";

+ 349 - 0
kxmall-admin-api/src/main/java/com/kxmall/web/controller/rider/service/impl/SmartDispatchServiceImpl.java

@@ -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();
+    }
+}

+ 19 - 2
kxmall-app-api/src/main/java/com/kxmall/web/controller/callback/CallbackController.java

@@ -1,24 +1,31 @@
 package com.kxmall.web.controller.callback;
 
 import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.ListUtil;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
+import com.kxmall.common.constant.CacheConstants;
 import com.kxmall.common.enums.OrderStatusType;
 import com.kxmall.common.enums.PayMethodEnum;
+import com.kxmall.common.utils.redis.RedisUtils;
 import com.kxmall.executor.GlobalExecutor;
 import com.kxmall.group.mapper.KxGroupShopMapper;
 import com.kxmall.notify.AdminNotifyBizService;
 import com.kxmall.order.biz.OrderBizService;
+import com.kxmall.order.biz.bo.SmartDispatchParamBo;
 import com.kxmall.order.domain.KxStoreOrder;
 import com.kxmall.order.domain.KxStoreOrderProduct;
 import com.kxmall.order.domain.vo.KxStoreOrderProductVo;
 import com.kxmall.order.domain.vo.KxStoreOrderVo;
+import com.kxmall.order.mapper.KxStoreOrderMapper;
 import com.kxmall.order.mapper.KxStoreOrderProductMapper;
 import com.kxmall.print.AdminPrintBizService;
+import com.kxmall.product.domain.vo.KxStoreProductVo;
 import com.kxmall.product.mapper.KxStoreProductMapper;
 import com.kxmall.web.controller.order.service.IKxAppOrderService;
 import com.kxmall.wechat.WxPayConfiguration;
@@ -34,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController;
 import java.math.BigDecimal;
 import java.util.Date;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * @author admin
@@ -59,6 +67,8 @@ public class CallbackController {
     private AdminNotifyBizService adminNotifyBizService;
     @Autowired
     private AdminPrintBizService adminPrintBizService;
+    @Autowired
+    private KxStoreOrderMapper kxStoreOrderMapper;
 
     @RequestMapping("/wxpay")
     @SaIgnore
@@ -130,6 +140,7 @@ public class CallbackController {
 
         List<KxStoreOrderProductVo> orderProducts = orderProductMapper.selectVoList(new QueryWrapper<KxStoreOrderProduct>().eq("order_id", order.getId()));
         order.setProductList(orderProducts);
+        List<Long> productIds = ListUtil.toList();
         orderProducts.forEach(item -> {
             // 增加销量
             storeProductMapper.incSales(item.getProductId(), item.getNum());
@@ -137,15 +148,21 @@ public class CallbackController {
                 // 增加团购人数, 若想算商品数这里就获取orderSku的数量,若想算人数,这里就写1
                 groupShopMapper.incCurrentNum(order.getCombinationId(), item.getNum());
             }
+            productIds.add(item.getProductId());
         });
 
+
+        List<String> keyWords = storeProductMapper.selectVoBatchIds(productIds).stream().map(KxStoreProductVo::getKeyword).collect(Collectors.toList());
+        SmartDispatchParamBo smartDispatchParamBo = new SmartDispatchParamBo(BeanUtil.toBean(order, KxStoreOrder.class), keyWords);
+        RedisUtils.setCacheMapValue(CacheConstants.WAIT_DISPATCH_ORDERS, order.getId().toString(), smartDispatchParamBo);
+
+
         // 通知管理员发货
         GlobalExecutor.execute(() -> {
             adminNotifyBizService.newOrder(order);
             adminPrintBizService.newOrderPrint(order);
         });
 
-
         return WxPayNotifyResponse.success("支付成功");
     }
 
@@ -185,9 +202,9 @@ public class CallbackController {
         orderBizService.changeOrderStatus(orderNo, OrderStatusType.UNPAY.getCode(), updateOrderDO);
 
         // 扣款
-
         List<KxStoreOrderProductVo> orderProducts = orderProductMapper.selectVoList(new QueryWrapper<KxStoreOrderProduct>().eq("order_id", order.getId()));
         order.setProductList(orderProducts);
+
         orderProducts.forEach(item -> {
             // 增加销量
             storeProductMapper.incSales(item.getProductId(), item.getNum());

+ 5 - 0
kxmall-app-api/src/main/java/com/kxmall/web/controller/order/builder/OrderBuilder.java

@@ -4,6 +4,9 @@ package com.kxmall.web.controller.order.builder;
 import com.kxmall.order.domain.KxStoreOrder;
 import com.kxmall.order.domain.bo.OrderPriceBo;
 import com.kxmall.order.domain.bo.OrderRequestBo;
+import com.kxmall.order.domain.bo.OrderRequestProductBo;
+
+import java.util.List;
 
 /**
  * @description: 抽象建造者
@@ -62,4 +65,6 @@ public abstract class OrderBuilder {
      * @param orderDO
      */
     public abstract void buildCallBackHandlePointsPart(KxStoreOrder orderDO);
+
+    public abstract void addOrderToPool(List<OrderRequestProductBo> productList, KxStoreOrder orderDO);
 }

+ 12 - 0
kxmall-app-api/src/main/java/com/kxmall/web/controller/order/builder/OrderConcreteBuilder.java

@@ -5,9 +5,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.kxmall.address.domain.KxAddress;
 import com.kxmall.address.mapper.KxAddressMapper;
+import com.kxmall.common.constant.CacheConstants;
 import com.kxmall.common.enums.*;
 import com.kxmall.common.exception.ServiceException;
 import com.kxmall.common.utils.GeneratorUtil;
+import com.kxmall.common.utils.redis.RedisUtils;
 import com.kxmall.coupon.domain.KxStoreCoupon;
 import com.kxmall.coupon.domain.KxStoreCouponUser;
 import com.kxmall.coupon.mapper.KxStoreCouponMapper;
@@ -18,6 +20,7 @@ import com.kxmall.group.domain.KxGroupShopProduct;
 import com.kxmall.group.domain.vo.KxGroupShopVo;
 import com.kxmall.group.mapper.KxGroupShopMapper;
 import com.kxmall.notify.AdminNotifyBizService;
+import com.kxmall.order.biz.bo.SmartDispatchParamBo;
 import com.kxmall.order.domain.KxStoreCart;
 import com.kxmall.order.domain.KxStoreOrder;
 import com.kxmall.order.domain.KxStoreOrderProduct;
@@ -686,4 +689,13 @@ public class OrderConcreteBuilder extends OrderBuilder {
             logger.error("保存订单照片失败, orderId: {}, error: {}", orderDO.getOrderId(), e.getMessage(), e);
         }
     }
+
+    @Override
+    public void addOrderToPool(List<OrderRequestProductBo> productList, KxStoreOrder orderDO) {
+        List<Long> productIds = productList.stream().map(OrderRequestProductBo::getProductId).collect(Collectors.toList());
+        List<String> keyWords = productMapper.selectVoBatchIds(productIds).stream().map(KxStoreProductVo::getKeyword).collect(Collectors.toList());
+        SmartDispatchParamBo smartDispatchParamBo = new SmartDispatchParamBo(orderDO, keyWords);
+        RedisUtils.setCacheMapValue(CacheConstants.WAIT_DISPATCH_ORDERS, orderDO.getId().toString(), smartDispatchParamBo);
+    }
+
 }

+ 1 - 0
kxmall-app-api/src/main/java/com/kxmall/web/controller/order/builder/OrderDirector.java

@@ -34,6 +34,7 @@ public class OrderDirector {
         // 余额回调
         if (PayChannelType.BALANCE.getCode().equals(orderRequest.getPayType())) {
             builder.buildCallBackHandlePart(orderDO);
+            builder.addOrderToPool(orderRequest.getProductList(), orderDO);
         }
         // 积分回调
         if (PayChannelType.INTEGRAL.getCode().equals(orderRequest.getPayType())) {

+ 2 - 2
kxmall-app-api/src/main/java/com/kxmall/web/controller/quartz/CheckQuartz.java

@@ -1,6 +1,5 @@
 package com.kxmall.web.controller.quartz;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.kxmall.common.enums.GroupShopAutomaticRefundType;
 import com.kxmall.common.enums.OrderStatusType;
@@ -26,7 +25,6 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionCallbackWithoutResult;
 import org.springframework.transaction.support.TransactionTemplate;
 import org.springframework.util.CollectionUtils;
-import org.springframework.util.ObjectUtils;
 
 import java.util.Date;
 import java.util.List;
@@ -63,6 +61,7 @@ public class CheckQuartz {
     @Autowired
     private IKxAppOrderService appOrderService;
 
+
     /**
      * 订单状态定时轮训
      */
@@ -279,4 +278,5 @@ public class CheckQuartz {
         }
     }
 
+
 }

+ 2 - 0
kxmall-common/src/main/java/com/kxmall/common/constant/CacheConstants.java

@@ -46,4 +46,6 @@ public interface CacheConstants {
      * 登录账户密码错误次数 redis key
      */
     String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+
+    String WAIT_DISPATCH_ORDERS = "wait_dispatch_orders";
 }

+ 19 - 0
kxmall-common/src/main/java/com/kxmall/common/enums/BooleanE.java

@@ -0,0 +1,19 @@
+package com.kxmall.common.enums;
+
+import lombok.Getter;
+
+/**
+ * 是非枚举
+ */
+@Getter
+public enum BooleanE {
+    Yes(1),
+    No(0);
+
+    private final Integer id;
+
+    BooleanE(Integer id) {
+        this.id = id;
+    }
+
+}

+ 0 - 24
kxmall-common/src/main/java/com/kxmall/common/enums/RiderWeekStatusType.java

@@ -1,24 +0,0 @@
-package com.kxmall.common.enums;
-
-public enum RiderWeekStatusType {
-    REST(0, "休息中"),
-    BUSINESS(1, "开工中");
-
-    private int code;
-
-    private String msg;
-
-    RiderWeekStatusType(int code, String msg) {
-        this.code = code;
-        this.msg = msg;
-    }
-
-    public int getCode() {
-        return code;
-    }
-
-
-    public String getMsg() {
-        return msg;
-    }
-}

+ 10 - 12
kxmall-common/src/main/java/com/kxmall/common/enums/RiderWorkStateType.java

@@ -1,31 +1,29 @@
 package com.kxmall.common.enums;
 
+import lombok.Getter;
+
 /**
  * @description: 配送员休息开工枚举
  * @author: kxmall
  * @date: 2020/02/28 19:58
  **/
-public enum  RiderWorkStateType {
+@Getter
+public enum RiderWorkStateType {
+
+    REST(0, "休息"),
 
-    REST(0,"休息"),
+    WORKING(1, "开工"),
 
-    WORKING(1,"开工");
+    IN_WORK(2, "服务中");
 
-    private int code;
+    private final int code;
 
-    private String msg;
+    private final String msg;
 
     RiderWorkStateType(int code, String msg) {
         this.code = code;
         this.msg = msg;
     }
 
-    public int getCode() {
-        return code;
-    }
 
-
-    public String getMsg() {
-        return msg;
-    }
 }

+ 10 - 0
kxmall-common/src/main/java/com/kxmall/common/exception/KxRuntimeException.java

@@ -0,0 +1,10 @@
+package com.kxmall.common.exception;
+
+public class KxRuntimeException extends RuntimeException {
+
+    private static final long serialVersionUID = 2764096393863468678L;
+
+    public KxRuntimeException(String message) {
+        super(message);
+    }
+}

+ 37 - 0
kxmall-common/src/main/java/com/kxmall/common/service/RiderNotificationService.java

@@ -0,0 +1,37 @@
+package com.kxmall.common.service;
+
+
+/**
+ * 骑手通知服务接口
+ * 通用模块接口,用于不同模块间的解耦
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+public interface RiderNotificationService {
+
+    /**
+     * 发送新订单通知给推荐的骑手列表
+     * 
+     * @param dispatchParam 派单参数
+     * @param riderIds 推荐的骑手ID列表
+     * @param matchScores 对应的匹配度分数
+     */
+    // void sendNewOrderNotification(SmartDispatchParamBo dispatchParam, List<String> riderIds, List<Double> matchScores);
+
+    /**
+     * 发送订单取消通知
+     * 
+     * @param orderNo 订单号
+     * @param riderId 骑手ID
+     */
+    void sendOrderCancelNotification(String orderNo, String riderId);
+
+    /**
+     * 检查骑手是否在线
+     * 
+     * @param riderId 骑手ID
+     * @return 是否在线
+     */
+    boolean isRiderOnline(String riderId);
+}

+ 12 - 8
kxmall-common/src/main/java/com/kxmall/common/utils/redis/RedisUtils.java

@@ -1,17 +1,12 @@
 package com.kxmall.common.utils.redis;
 
-import com.kxmall.common.exception.ServiceException;
 import com.kxmall.common.utils.spring.SpringUtils;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import org.redisson.api.*;
 
 import java.time.Duration;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
+import java.util.*;
 import java.util.concurrent.locks.Lock;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
@@ -28,6 +23,7 @@ import java.util.stream.Stream;
 public class RedisUtils {
 
     private static final RedissonClient CLIENT = SpringUtils.getBean(RedissonClient.class);
+    private static final String LOCK_PREFIX = "LOCK_PREFIX_";
 
     /**
      * 限流
@@ -55,8 +51,6 @@ public class RedisUtils {
         return CLIENT;
     }
 
-    private static final String LOCK_PREFIX = "LOCK_PREFIX_";
-
     public static Lock lock(String key) {
         return CLIENT.getLock(LOCK_PREFIX + key);
     }
@@ -469,4 +463,14 @@ public class RedisUtils {
         RKeys rKeys = CLIENT.getKeys();
         return rKeys.countExists(key) > 0;
     }
+
+    public static void addToSet(String key, Object... values) {
+        RSet<Object> set = CLIENT.getSet(key);
+        set.addAll(Arrays.stream(values).collect(Collectors.toList()));
+    }
+
+    public static void removeToSet(String key, Object value) {
+        RSet<Object> set = CLIENT.getSet(key);
+        set.remove(value);
+    }
 }

+ 6 - 0
kxmall-framework/pom.xml

@@ -61,6 +61,12 @@
             <artifactId>transmittable-thread-local</artifactId>
         </dependency>
 
+        <!-- WebSocket支持 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
         <!-- 系统模块-->
         <dependency>
             <groupId>com.kxmall</groupId>

+ 24 - 0
kxmall-framework/src/main/java/com/kxmall/framework/config/WebSocketConfig.java

@@ -0,0 +1,24 @@
+package com.kxmall.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+/**
+ * WebSocket配置
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+@Configuration
+public class WebSocketConfig {
+
+    /**
+     * 注入ServerEndpointExporter,
+     * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
+     */
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+}

+ 1 - 1
kxmall-generator/src/test/java/Gen.java

@@ -27,7 +27,7 @@ public class Gen {
                                 .pathInfo(Collections.singletonMap(OutputFile.xml, "D://gen")) // 设置mapperXml生成路径
                 )
                 .strategyConfig(builder ->
-                        builder.addInclude("kx_order_snapshot").entityBuilder().enableLombok()
+                        builder.addInclude("kx_rider_ext").entityBuilder().enableLombok()
                 )
                 .execute();
     }

+ 15 - 7
kxmall-rider-api/src/main/java/com/kxmall/web/controller/quartz/CheckRiderQuartz.java

@@ -4,8 +4,8 @@ import com.kxmall.common.enums.RiderOrderStatusType;
 import com.kxmall.common.utils.redis.RedisUtils;
 import com.kxmall.order.biz.OrderRiderBizService;
 import com.kxmall.rider.mapper.KxRiderOrderMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import com.kxmall.web.controller.quartz.service.RiderQuartzService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.scheduling.annotation.Scheduled;
@@ -23,17 +23,20 @@ import java.util.concurrent.locks.Lock;
  */
 @Component
 @EnableScheduling
+@Slf4j
 public class CheckRiderQuartz {
 
-    private static final Logger logger = LoggerFactory.getLogger(CheckRiderQuartz.class);
-
     private static final String RIDER_ORDER_STATUS_LOCK = "RIDER_ORDER_STATUS_QUARTZ_LOCK";
 
     @Autowired
     private KxRiderOrderMapper riderOrderMapper;
+
     @Autowired
     private OrderRiderBizService orderRiderBizService;
 
+    @Autowired
+    private RiderQuartzService riderQuartzService;
+
 
     /**
      * 订单状态定时轮训
@@ -53,18 +56,23 @@ public class CheckRiderQuartz {
                             try {
                                 orderRiderBizService.sendRiderMessageBusiness(no, RiderOrderStatusType.TIMEOUT, 0L, "");
                             } catch (Exception e) {
-                                logger.error("[订单状态变更异常] 异常", e);
+                                log.error("[订单状态变更异常] 异常", e);
                             }
                         });
                     }
                 } catch (Exception e) {
-                    logger.error("[骑手订单状态检测定时任务] 异常", e);
+                    log.error("[骑手订单状态检测定时任务] 异常", e);
                 } finally {
                     lock.unlock();
                 }
             }
         } catch (InterruptedException e) {
-            e.printStackTrace();
+            log.error(e.getMessage(), e);
         }
     }
+
+    @Scheduled(cron = "10 0 0 * * ?")
+    public void flushRiderExtInfo() {
+        riderQuartzService.flushRiderExtInfo();
+    }
 }

+ 12 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/quartz/service/RiderQuartzService.java

@@ -0,0 +1,12 @@
+package com.kxmall.web.controller.quartz.service;
+
+/**
+ *
+ * @author tea
+ * @date 2025/9/6
+ */
+public interface RiderQuartzService {
+
+    void flushRiderExtInfo();
+
+}

+ 76 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/quartz/service/impl/RiderQuartzServiceImpl.java

@@ -0,0 +1,76 @@
+package com.kxmall.web.controller.quartz.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.kxmall.common.enums.RiderOrderStatusType;
+import com.kxmall.order.domain.KxStoreAppraise;
+import com.kxmall.order.mapper.KxStoreAppraiseMapper;
+import com.kxmall.rider.domain.KxRiderOrder;
+import com.kxmall.rider.mapper.KxRiderExtMapper;
+import com.kxmall.rider.mapper.KxRiderMapper;
+import com.kxmall.rider.mapper.KxRiderOrderMapper;
+import com.kxmall.web.controller.quartz.service.RiderQuartzService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @author tea
+ * @date 2025/9/6
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class RiderQuartzServiceImpl implements RiderQuartzService {
+
+    private final KxRiderMapper kxRiderMapper;
+    private final KxRiderExtMapper kxRiderExtMapper;
+    private final KxRiderOrderMapper kxRiderOrderMapper;
+    private final KxStoreAppraiseMapper appraiseMapper;
+
+    @Override
+    public void flushRiderExtInfo() {
+        List<KxRiderOrder> finishedOrderList = kxRiderOrderMapper.selectFinishedOrderListByLastDay(RiderOrderStatusType.COMPLETED, LocalDateTime.now());
+        Map<Long, List<KxRiderOrder>> finishedOrderMap = finishedOrderList.stream().collect(Collectors.groupingBy(KxRiderOrder::getRiderId));
+
+        List<Long> orderIds = finishedOrderList.stream().map(KxRiderOrder::getOrderId).collect(Collectors.toList());
+        LambdaQueryWrapper<KxStoreAppraise> appraiseLqw = new LambdaQueryWrapper<>();
+        appraiseLqw.in(KxStoreAppraise::getOrderId, orderIds);
+        List<KxStoreAppraise> appraises = appraiseMapper.selectList(appraiseLqw);
+        Map<Long, List<KxStoreAppraise>> appraiseMap = appraises.stream().collect(Collectors.groupingBy(KxStoreAppraise::getOrderId));
+
+        finishedOrderMap.forEach((riderId, riderOrders) -> {
+            // 订单数
+            int totalCount = riderOrders.size();
+            // 总评价
+            int totalReviews = 0;
+            // 订单分数
+            BigDecimal totalScore = BigDecimal.ZERO;
+
+            for (KxRiderOrder order : riderOrders) {
+                // 如果有一个订单有评价则+1
+                List<KxStoreAppraise> orderAppraises = appraiseMap.get(order.getOrderId());
+                if (CollUtil.isNotEmpty(orderAppraises)) {
+                    totalReviews += 1;
+
+                    long score = orderAppraises.stream().mapToLong(e -> Objects.isNull(e.getScore()) ? 0 : e.getScore()).sum();
+                    BigDecimal d = BigDecimal.valueOf(score).divide(BigDecimal.valueOf(orderAppraises.size()), 4, RoundingMode.HALF_UP);
+                    totalScore = totalScore.add(d);
+                }
+            }
+
+            kxRiderExtMapper.updateStore(totalCount, totalReviews, totalScore, riderId);
+        });
+
+        kxRiderMapper.updateRestDayForAll();
+    }
+}

+ 13 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/task/service/impl/TaskCenterServiceImpl.java

@@ -3,11 +3,13 @@ package com.kxmall.web.controller.task.service.impl;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.kxmall.common.constant.CacheConstants;
 import com.kxmall.common.core.domain.PageQuery;
 import com.kxmall.common.core.page.TableDataInfo;
 import com.kxmall.common.enums.RiderOrderStatusType;
 import com.kxmall.common.enums.RiderTransactionSource;
 import com.kxmall.common.enums.RiderTransactionType;
+import com.kxmall.common.enums.RiderWorkStateType;
 import com.kxmall.common.exception.ServiceException;
 import com.kxmall.common.utils.redis.RedisUtils;
 import com.kxmall.order.biz.OrderRiderBizService;
@@ -139,7 +141,13 @@ public class TaskCenterServiceImpl implements TaskCenterService {
                 riderOrderDO.setStatus(RiderOrderStatusType.DISPENSE.getCode());
                 riderOrderDO.setUpdateTime(new Date());
                 if (riderOrderMapper.updateById(riderOrderDO) > 0) {
+                    KxRider kxRider = new KxRider();
+                    kxRider.setId(riderId);
+                    kxRider.setWorkState(RiderWorkStateType.IN_WORK.getCode());
+                    riderMapper.updateById(kxRider);
                     orderRiderBizService.sendRiderMessageBusiness(riderOrderDO.getOrderNo(), RiderOrderStatusType.DISPENSE, riderOrderDO.getRiderId(), null);
+
+                    RedisUtils.delCacheMapValue(CacheConstants.WAIT_DISPATCH_ORDERS, riderOrderDO.getOrderId().toString());
                     return "ok";
                 }
                 throw new ServiceException("配送员订单状态更新异常,请稍后再试");
@@ -211,6 +219,11 @@ public class TaskCenterServiceImpl implements TaskCenterService {
                     if (riderOrderDO.getFreightPrice() != null && riderOrderDO.getFreightPrice().compareTo(BigDecimal.ZERO) > 0) {
                         updateRiderWallet(riderOrderDO.getRiderId(), riderOrderDO.getFreightPrice(), riderOrderDO.getOrderNo());
                     }
+
+                    KxRider kxRider = new KxRider();
+                    kxRider.setId(riderId);
+                    kxRider.setWorkState(RiderWorkStateType.WORKING.getCode());
+                    riderMapper.updateById(kxRider);
                     orderRiderBizService.sendRiderMessageBusiness(riderOrderDO.getOrderNo(), RiderOrderStatusType.COMPLETED, riderOrderDO.getRiderId(), null);
                     return "ok";
                 }

+ 103 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/RiderWebSocketController.java

@@ -0,0 +1,103 @@
+package com.kxmall.web.controller.websocket;
+
+import com.kxmall.common.core.domain.R;
+import com.kxmall.web.controller.websocket.dto.OrderNotificationDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+
+/**
+ * 骑手WebSocket管理控制器
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+@RestController
+@RequestMapping("/websocket/rider")
+@Slf4j
+public class RiderWebSocketController {
+
+    /**
+     * 获取在线骑手数量
+     */
+    @GetMapping("/online/count")
+    public R<Integer> getOnlineCount() {
+        int count = RiderWebSocketServer.getOnlineCount();
+        return R.ok(count);
+    }
+
+    /**
+     * 获取所有在线骑手ID
+     */
+    @GetMapping("/online/riders")
+    public R<String[]> getOnlineRiders() {
+        String[] riderIds = RiderWebSocketServer.getOnlineRiderIds();
+        return R.ok(riderIds);
+    }
+
+    /**
+     * 检查指定骑手是否在线
+     */
+    @GetMapping("/online/check/{riderId}")
+    public R<Boolean> checkRiderOnline(@PathVariable String riderId) {
+        boolean isOnline = RiderWebSocketServer.isRiderOnline(riderId);
+        return R.ok(isOnline);
+    }
+
+    /**
+     * 向指定骑手发送消息(测试用)
+     */
+    @PostMapping("/send/{riderId}")
+    public R<Void> sendMessageToRider(@PathVariable String riderId, @RequestBody String message) {
+        try {
+            RiderWebSocketServer.sendInfo(message, riderId);
+            log.info("向骑手{}发送消息成功: {}", riderId, message);
+            return R.ok();
+        } catch (Exception e) {
+            log.error("向骑手{}发送消息失败", riderId, e);
+            return R.fail("发送消息失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 向所有在线骑手广播消息
+     */
+    @PostMapping("/broadcast")
+    public R<Void> broadcastMessage(@RequestBody String message) {
+        try {
+            RiderWebSocketServer.sendToAll(message);
+            log.info("广播消息成功: {}", message);
+            return R.ok();
+        } catch (Exception e) {
+            log.error("广播消息失败", e);
+            return R.fail("广播消息失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 发送测试订单通知
+     */
+    @PostMapping("/test/order/{riderId}")
+    public R<Void> sendTestOrderNotification(@PathVariable String riderId) {
+        try {
+            OrderNotificationDTO notification = new OrderNotificationDTO();
+            notification.setType(OrderNotificationDTO.Type.NEW_ORDER);
+            notification.setOrderNo("TEST" + System.currentTimeMillis());
+            notification.setStoreName("测试仓库");
+            notification.setConsignee("测试用户");
+            notification.setPhone("138****8888");
+            notification.setAddress("测试地址");
+            notification.setCreateTime(new Date());
+            notification.setRemark("这是一个测试订单通知");
+            notification.setMatchScore(85.5);
+
+            RiderWebSocketServer.sendOrderNotification(riderId, notification);
+            log.info("向骑手{}发送测试订单通知成功", riderId);
+            return R.ok();
+        } catch (Exception e) {
+            log.error("向骑手{}发送测试订单通知失败", riderId, e);
+            return R.fail("发送测试通知失败: " + e.getMessage());
+        }
+    }
+}

+ 187 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/RiderWebSocketServer.java

@@ -0,0 +1,187 @@
+package com.kxmall.web.controller.websocket;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.*;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 骑手WebSocket服务端点
+ * 用于实时推送订单通知给骑手
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+@Component
+@Slf4j
+@ServerEndpoint("/websocket/rider/{riderId}")
+public class RiderWebSocketServer {
+
+    /** 静态变量,用来记录当前在线连接数 */
+    private static final AtomicInteger onlineCount = new AtomicInteger(0);
+
+    /** concurrent包的线程安全Map,用来存放每个客户端对应的RiderWebSocketServer对象 */
+    private static final ConcurrentHashMap<String, RiderWebSocketServer> webSocketMap = new ConcurrentHashMap<>();
+
+    /** 与某个客户端的连接会话,需要通过它来给客户端发送数据 */
+    private Session session;
+
+    /** 接收riderId */
+    private String riderId = "";
+
+    /**
+     * 连接建立成功调用的方法
+     */
+    @OnOpen
+    public void onOpen(Session session, @PathParam("riderId") String riderId) {
+        this.session = session;
+        this.riderId = riderId;
+        
+        if (webSocketMap.containsKey(riderId)) {
+            webSocketMap.remove(riderId);
+            webSocketMap.put(riderId, this);
+        } else {
+            webSocketMap.put(riderId, this);
+            addOnlineCount();
+        }
+
+        log.info("骑手{}连接WebSocket成功,当前在线人数为:{}", riderId, getOnlineCount());
+        
+        try {
+            sendMessage("连接成功");
+        } catch (IOException e) {
+            log.error("骑手{},网络异常!!!!!!", riderId, e);
+        }
+    }
+
+    /**
+     * 连接关闭调用的方法
+     */
+    @OnClose
+    public void onClose() {
+        if (webSocketMap.containsKey(riderId)) {
+            webSocketMap.remove(riderId);
+            subOnlineCount();
+        }
+        log.info("骑手{}断开WebSocket连接!当前在线人数为:{}", riderId, getOnlineCount());
+    }
+
+    /**
+     * 收到客户端消息后调用的方法
+     *
+     * @param message 客户端发送过来的消息
+     */
+    @OnMessage
+    public void onMessage(String message, Session session) {
+        log.info("收到来自骑手{}的信息:{}", riderId, message);
+        
+        // 可以处理心跳消息或其他业务消息
+        if ("ping".equals(message)) {
+            try {
+                sendMessage("pong");
+            } catch (IOException e) {
+                log.error("发送心跳响应失败", e);
+            }
+        }
+    }
+
+    /**
+     * 发生错误时调用
+     */
+    @OnError
+    public void onError(Session session, Throwable error) {
+        log.error("骑手{}的WebSocket发生错误", riderId, error);
+    }
+
+    /**
+     * 实现服务器主动推送
+     */
+    public void sendMessage(String message) throws IOException {
+        this.session.getBasicRemote().sendText(message);
+    }
+
+    /**
+     * 发送自定义消息
+     */
+    public static void sendInfo(String message, @PathParam("riderId") String riderId) throws IOException {
+        log.info("发送消息到骑手{},报文:{}", riderId, message);
+        
+        if (StrUtil.isNotBlank(riderId) && webSocketMap.containsKey(riderId)) {
+            webSocketMap.get(riderId).sendMessage(message);
+        } else {
+            log.error("骑手{}不在线!", riderId);
+        }
+    }
+
+    /**
+     * 发送订单通知给指定骑手
+     */
+    public static void sendOrderNotification(String riderId, Object orderData) {
+        try {
+            if (StrUtil.isNotBlank(riderId) && webSocketMap.containsKey(riderId)) {
+                String jsonMessage = JSONUtil.toJsonStr(orderData);
+                webSocketMap.get(riderId).sendMessage(jsonMessage);
+                log.info("成功发送订单通知给骑手:{}", riderId);
+            } else {
+                log.warn("骑手{}不在线,无法发送订单通知", riderId);
+            }
+        } catch (IOException e) {
+            log.error("发送订单通知给骑手{}失败", riderId, e);
+        }
+    }
+
+    /**
+     * 广播消息给所有在线骑手
+     */
+    public static void sendToAll(String message) {
+        for (String key : webSocketMap.keySet()) {
+            try {
+                webSocketMap.get(key).sendMessage(message);
+            } catch (IOException e) {
+                log.error("广播消息给骑手{}失败", key, e);
+            }
+        }
+    }
+
+    /**
+     * 检查骑手是否在线
+     */
+    public static boolean isRiderOnline(String riderId) {
+        return StrUtil.isNotBlank(riderId) && webSocketMap.containsKey(riderId);
+    }
+
+    /**
+     * 获取在线骑手数量
+     */
+    public static synchronized int getOnlineCount() {
+        return onlineCount.get();
+    }
+
+    /**
+     * 在线数加1
+     */
+    public static synchronized void addOnlineCount() {
+        onlineCount.incrementAndGet();
+    }
+
+    /**
+     * 在线数减1
+     */
+    public static synchronized void subOnlineCount() {
+        onlineCount.decrementAndGet();
+    }
+
+    /**
+     * 获取所有在线骑手ID
+     */
+    public static String[] getOnlineRiderIds() {
+        return webSocketMap.keySet().toArray(new String[0]);
+    }
+}

+ 119 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/dto/OrderNotificationDTO.java

@@ -0,0 +1,119 @@
+package com.kxmall.web.controller.websocket.dto;
+
+import lombok.Data;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单通知DTO
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+@Data
+@ToString
+public class OrderNotificationDTO implements Serializable {
+
+    /**
+     * 通知类型
+     */
+    private String type;
+
+    /**
+     * 订单ID
+     */
+    private Long orderId;
+
+    /**
+     * 订单编号
+     */
+    private String orderNo;
+
+    /**
+     * 仓库名称
+     */
+    private String storeName;
+
+    /**
+     * 配送费
+     */
+    private BigDecimal freightPrice;
+
+    /**
+     * 订单总金额
+     */
+    private BigDecimal totalAmount;
+
+    /**
+     * 收货人
+     */
+    private String consignee;
+
+    /**
+     * 收货电话
+     */
+    private String phone;
+
+    /**
+     * 收货地址
+     */
+    private String address;
+
+    /**
+     * 经度
+     */
+    private BigDecimal longitude;
+
+    /**
+     * 纬度
+     */
+    private BigDecimal latitude;
+
+    /**
+     * 预计送达时间
+     */
+    private Date predictTime;
+
+    /**
+     * 紧急程度
+     * 1-普通 2-紧急 3-特急
+     */
+    private Integer urgencyLevel;
+
+    /**
+     * 服务类型关键词
+     */
+    private String serviceKeywords;
+
+    /**
+     * 匹配度分数
+     */
+    private Double matchScore;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 通知类型常量
+     */
+    public static class Type {
+        /** 新订单通知 */
+        public static final String NEW_ORDER = "NEW_ORDER";
+        /** 订单取消通知 */
+        public static final String ORDER_CANCEL = "ORDER_CANCEL";
+        /** 订单状态更新 */
+        public static final String ORDER_STATUS_UPDATE = "ORDER_STATUS_UPDATE";
+        /** 系统消息 */
+        public static final String SYSTEM_MESSAGE = "SYSTEM_MESSAGE";
+    }
+}

+ 62 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/service/RiderNotificationService.java

@@ -0,0 +1,62 @@
+package com.kxmall.web.controller.websocket.service;
+
+import com.kxmall.order.biz.bo.SmartDispatchParamBo;
+import com.kxmall.rider.domain.vo.KxRiderVo;
+import com.kxmall.web.controller.websocket.dto.OrderNotificationDTO;
+
+import java.util.List;
+
+/**
+ * 骑手通知服务接口
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+public interface RiderNotificationService {
+
+    /**
+     * 发送新订单通知给推荐的骑手列表
+     * 
+     * @param dispatchParam 派单参数
+     * @param recommendedRiders 推荐的骑手列表
+     */
+    void sendNewOrderNotification(SmartDispatchParamBo dispatchParam, List<KxRiderVo> recommendedRiders);
+
+    /**
+     * 发送订单通知给指定骑手
+     * 
+     * @param riderId 骑手ID
+     * @param notification 通知内容
+     */
+    void sendOrderNotification(String riderId, OrderNotificationDTO notification);
+
+    /**
+     * 发送订单取消通知
+     * 
+     * @param orderNo 订单号
+     * @param riderId 骑手ID
+     */
+    void sendOrderCancelNotification(String orderNo, String riderId);
+
+    /**
+     * 发送系统消息给所有在线骑手
+     * 
+     * @param message 系统消息
+     */
+    void sendSystemMessage(String message);
+
+    /**
+     * 检查骑手是否在线
+     * 
+     * @param riderId 骑手ID
+     * @return 是否在线
+     */
+    boolean isRiderOnline(String riderId);
+
+    /**
+     * 获取在线骑手数量
+     * 
+     * @return 在线数量
+     */
+    int getOnlineRiderCount();
+}

+ 114 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/service/impl/CommonRiderNotificationServiceImpl.java

@@ -0,0 +1,114 @@
+package com.kxmall.web.controller.websocket.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.kxmall.common.service.RiderNotificationService;
+import com.kxmall.order.biz.bo.SmartDispatchParamBo;
+import com.kxmall.web.controller.websocket.RiderWebSocketServer;
+import com.kxmall.web.controller.websocket.dto.OrderNotificationDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+
+/**
+ * 通用骑手通知服务实现类
+ * 实现通用模块接口,供其他模块调用
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+@Service
+@Slf4j
+public class CommonRiderNotificationServiceImpl implements RiderNotificationService {
+
+    // @Override
+    // public void sendNewOrderNotification(SmartDispatchParamBo dispatchParam, List<String> riderIds, List<Double> matchScores) {
+    //     if (dispatchParam == null || CollUtil.isEmpty(riderIds)) {
+    //         log.warn("派单参数或骑手ID列表为空,无法发送通知");
+    //         return;
+    //     }
+    //
+    //     // 创建订单通知
+    //     OrderNotificationDTO notification = createOrderNotification(dispatchParam);
+    //
+    //     // 只发送给在线的骑手
+    //     int sentCount = 0;
+    //     for (int i = 0; i < riderIds.size(); i++) {
+    //         String riderId = riderIds.get(i);
+    //         if (isRiderOnline(riderId)) {
+    //             // 设置匹配度分数
+    //             if (matchScores != null && i < matchScores.size()) {
+    //                 notification.setMatchScore(matchScores.get(i));
+    //             }
+    //
+    //             try {
+    //                 RiderWebSocketServer.sendOrderNotification(riderId, notification);
+    //                 sentCount++;
+    //                 log.debug("成功发送订单通知给骑手:{}", riderId);
+    //             } catch (Exception e) {
+    //                 log.error("发送通知给骑手{}失败", riderId, e);
+    //             }
+    //         } else {
+    //             log.debug("骑手{}不在线,跳过通知发送", riderId);
+    //         }
+    //     }
+    //
+    //     log.info("订单{}成功发送通知给{}个在线骑手", dispatchParam.getOrderNo(), sentCount);
+    // }
+
+    @Override
+    public void sendOrderCancelNotification(String orderNo, String riderId) {
+        if (StrUtil.isBlank(orderNo) || StrUtil.isBlank(riderId)) {
+            log.warn("订单号或骑手ID为空");
+            return;
+        }
+
+        OrderNotificationDTO notification = new OrderNotificationDTO();
+        notification.setType(OrderNotificationDTO.Type.ORDER_CANCEL);
+        notification.setOrderNo(orderNo);
+        notification.setCreateTime(new Date());
+        notification.setRemark("订单已取消");
+
+        try {
+            RiderWebSocketServer.sendOrderNotification(riderId, notification);
+            log.info("发送订单{}取消通知给骑手{}", orderNo, riderId);
+        } catch (Exception e) {
+            log.error("发送订单取消通知失败", e);
+        }
+    }
+
+    @Override
+    public boolean isRiderOnline(String riderId) {
+        return RiderWebSocketServer.isRiderOnline(riderId);
+    }
+
+    /**
+     * 创建订单通知对象
+     */
+    private OrderNotificationDTO createOrderNotification(SmartDispatchParamBo dispatchParam) {
+        OrderNotificationDTO notification = new OrderNotificationDTO();
+        // notification.setType(OrderNotificationDTO.Type.NEW_ORDER);
+        // notification.setOrderId(dispatchParam.getOrderId());
+        // notification.setOrderNo(dispatchParam.getOrderNo());
+        // notification.setStoreName(dispatchParam.getStoreName());
+        // notification.setFreightPrice(dispatchParam.getFreightPrice());
+        // notification.setTotalAmount(dispatchParam.getTotalAmount());
+        // notification.setConsignee(dispatchParam.getConsignee());
+        // notification.setPhone(dispatchParam.getPhone());
+        // notification.setAddress(dispatchParam.getAddress());
+        // notification.setLongitude(dispatchParam.getLongitude());
+        // notification.setLatitude(dispatchParam.getLatitude());
+        // notification.setPredictTime(dispatchParam.getPredictTime());
+        // notification.setUrgencyLevel(dispatchParam.getUrgencyLevel());
+        // notification.setCreateTime(new Date());
+        // notification.setRemark(dispatchParam.getRemark());
+        
+        // 服务关键词转换为字符串
+        if (CollUtil.isNotEmpty(dispatchParam.getServiceKeywords())) {
+            notification.setServiceKeywords(String.join(",", dispatchParam.getServiceKeywords()));
+        }
+
+        return notification;
+    }
+}

+ 147 - 0
kxmall-rider-api/src/main/java/com/kxmall/web/controller/websocket/service/impl/RiderNotificationServiceImpl.java

@@ -0,0 +1,147 @@
+package com.kxmall.web.controller.websocket.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.kxmall.order.biz.bo.SmartDispatchParamBo;
+import com.kxmall.rider.domain.vo.KxRiderVo;
+import com.kxmall.web.controller.websocket.RiderWebSocketServer;
+import com.kxmall.web.controller.websocket.dto.OrderNotificationDTO;
+import com.kxmall.web.controller.websocket.service.RiderNotificationService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 骑手通知服务实现类
+ * 
+ * @author kxmall
+ * @date 2025-01-08
+ */
+@Service
+@Slf4j
+public class RiderNotificationServiceImpl implements RiderNotificationService {
+
+    @Override
+    public void sendNewOrderNotification(SmartDispatchParamBo dispatchParam, List<KxRiderVo> recommendedRiders) {
+        if (dispatchParam == null || CollUtil.isEmpty(recommendedRiders)) {
+            log.warn("派单参数或推荐骑手列表为空,无法发送通知");
+            return;
+        }
+
+        // 创建订单通知
+        OrderNotificationDTO notification = createOrderNotification(dispatchParam);
+        
+        // 只发送给前5名匹配度最高的在线骑手
+        List<KxRiderVo> onlineRiders = recommendedRiders.stream()
+                .filter(rider -> isRiderOnline(String.valueOf(rider.getId())))
+                .limit(5)
+                .collect(Collectors.toList());
+
+        if (onlineRiders.isEmpty()) {
+            log.warn("订单{}没有找到在线的推荐骑手", dispatchParam.getOrderNo());
+            return;
+        }
+
+        // 发送通知
+        for (KxRiderVo rider : onlineRiders) {
+            notification.setMatchScore(rider.getMatchScore());
+            sendOrderNotification(String.valueOf(rider.getId()), notification);
+        }
+
+        log.info("订单{}成功发送通知给{}个在线骑手", dispatchParam.getOrderNo(), onlineRiders.size());
+    }
+
+    @Override
+    public void sendOrderNotification(String riderId, OrderNotificationDTO notification) {
+        if (StrUtil.isBlank(riderId) || notification == null) {
+            log.warn("骑手ID或通知内容为空");
+            return;
+        }
+
+        try {
+            RiderWebSocketServer.sendOrderNotification(riderId, notification);
+            log.debug("成功发送通知给骑手:{}", riderId);
+        } catch (Exception e) {
+            log.error("发送通知给骑手{}失败", riderId, e);
+        }
+    }
+
+    @Override
+    public void sendOrderCancelNotification(String orderNo, String riderId) {
+        if (StrUtil.isBlank(orderNo) || StrUtil.isBlank(riderId)) {
+            log.warn("订单号或骑手ID为空");
+            return;
+        }
+
+        OrderNotificationDTO notification = new OrderNotificationDTO();
+        notification.setType(OrderNotificationDTO.Type.ORDER_CANCEL);
+        notification.setOrderNo(orderNo);
+        notification.setCreateTime(new Date());
+        notification.setRemark("订单已取消");
+
+        sendOrderNotification(riderId, notification);
+        log.info("发送订单{}取消通知给骑手{}", orderNo, riderId);
+    }
+
+    @Override
+    public void sendSystemMessage(String message) {
+        if (StrUtil.isBlank(message)) {
+            log.warn("系统消息内容为空");
+            return;
+        }
+
+        OrderNotificationDTO notification = new OrderNotificationDTO();
+        notification.setType(OrderNotificationDTO.Type.SYSTEM_MESSAGE);
+        notification.setCreateTime(new Date());
+        notification.setRemark(message);
+
+        try {
+            RiderWebSocketServer.sendToAll(cn.hutool.json.JSONUtil.toJsonStr(notification));
+            log.info("成功广播系统消息:{}", message);
+        } catch (Exception e) {
+            log.error("广播系统消息失败", e);
+        }
+    }
+
+    @Override
+    public boolean isRiderOnline(String riderId) {
+        return RiderWebSocketServer.isRiderOnline(riderId);
+    }
+
+    @Override
+    public int getOnlineRiderCount() {
+        return RiderWebSocketServer.getOnlineCount();
+    }
+
+    /**
+     * 创建订单通知对象
+     */
+    private OrderNotificationDTO createOrderNotification(SmartDispatchParamBo dispatchParam) {
+        OrderNotificationDTO notification = new OrderNotificationDTO();
+        notification.setType(OrderNotificationDTO.Type.NEW_ORDER);
+        notification.setOrderId(dispatchParam.getOrderId());
+        notification.setOrderNo(dispatchParam.getOrderNo());
+        notification.setStoreName(dispatchParam.getStoreName());
+        notification.setFreightPrice(dispatchParam.getFreightPrice());
+        notification.setTotalAmount(dispatchParam.getTotalAmount());
+        notification.setConsignee(dispatchParam.getConsignee());
+        notification.setPhone(dispatchParam.getPhone());
+        notification.setAddress(dispatchParam.getAddress());
+        notification.setLongitude(dispatchParam.getLongitude());
+        notification.setLatitude(dispatchParam.getLatitude());
+        notification.setPredictTime(dispatchParam.getPredictTime());
+        notification.setUrgencyLevel(dispatchParam.getUrgencyLevel());
+        notification.setCreateTime(new Date());
+        notification.setRemark(dispatchParam.getRemark());
+        
+        // 服务关键词转换为字符串
+        if (CollUtil.isNotEmpty(dispatchParam.getServiceKeywords())) {
+            notification.setServiceKeywords(String.join(",", dispatchParam.getServiceKeywords()));
+        }
+
+        return notification;
+    }
+}

+ 68 - 73
kxmall-system/src/main/java/com/kxmall/order/biz/OrderBizService.java

@@ -45,11 +45,9 @@ import static com.kxmall.common.enums.Brokerage.LEVEL_1;
 @Service
 public class OrderBizService {
 
-    private static final String ORDER_STATUS_LOCK = "ORDER_STATUS_LOCK_";
-
-    //订单退款乐观锁
+    // 订单退款乐观锁
     public static final String ORDER_REFUND_LOCK = "ORDER_REFUND_LOCK_";
-
+    private static final String ORDER_STATUS_LOCK = "ORDER_STATUS_LOCK_";
     private static final Logger logger = LoggerFactory.getLogger(OrderBizService.class);
 
     @Autowired
@@ -69,10 +67,10 @@ public class OrderBizService {
     private BillBizService billBizService;
 
     @Autowired
-    private  ConfigService configService;
+    private ConfigService configService;
 
     @Autowired
-    private  KxStoreProductAttrValueMapper storeProductAttrValueMapper;
+    private KxStoreProductAttrValueMapper storeProductAttrValueMapper;
 
 
     public Boolean changeOrderStatus(String orderId, Integer nowStatus, KxStoreOrder updateOrderDO) {
@@ -158,13 +156,13 @@ public class OrderBizService {
         Lock lock = RedisUtils.lock(ORDER_REFUND_LOCK + orderNo);
         try {
             if (lock.tryLock(30, TimeUnit.SECONDS)) {
-                //1.校验订单状态是否处于团购状态中
+                // 1.校验订单状态是否处于团购状态中
                 KxStoreOrder orderDO = checkOrderExist(orderNo, null);
                 if (orderDO.getStatus() != OrderStatusType.GROUP_SHOP_WAIT.getCode()) {
                     throw new ServiceException("订单状态不是团购状态");
                 }
-                //2.退款处理
-                //2.1.1 先流转状态
+                // 2.退款处理
+                // 2.1.1 先流转状态
                 KxStoreOrder updateKxStoreOrder = KxStoreOrder.builder().build();
                 updateKxStoreOrder.setStatus(OrderStatusType.REFUNDED.getCode());
                 updateKxStoreOrder.setUpdateTime(new Date());
@@ -173,7 +171,7 @@ public class OrderBizService {
                 KxUser userDO = userMapper.selectById(userId);
                 Integer loginType = userDO.getLoginType();
 
-                //2.1.2 向微信支付平台发送退款请求
+                // 2.1.2 向微信支付平台发送退款请求
                 WxPayService wxPayService = WxPayConfiguration.getPayService(loginType == UserLoginType.MP_WEIXIN.getCode() ? PayMethodEnum.MINI : PayMethodEnum.APP);
                 WxPayRefundRequest wxPayRefundRequest = new WxPayRefundRequest();
                 wxPayRefundRequest.setOutTradeNo(orderNo);
@@ -208,6 +206,7 @@ public class OrderBizService {
 
     /**
      * 将冻结库存还回去
+     *
      * @param no
      */
     public void callbackStock(String no) {
@@ -219,86 +218,83 @@ public class OrderBizService {
     }
 
 
-
-
-
     public void backOrderBrokerage(KxStoreOrderVo order) {
-        //如果分销没开启直接返回
+        // 如果分销没开启直接返回
         String open = configService.getConfigValue(SystemConfigConstants.STORE_BROKERAGE_OPEN);
-        if(StrUtil.isBlank(open) || ShopCommonEnum.ENABLE_2.getValue().toString().equals(open)) {
+        if (StrUtil.isBlank(open) || ShopCommonEnum.ENABLE_2.getValue().toString().equals(open)) {
             return;
         }
 
-        //获取购买商品的用户
-        KxUser userInfo =  userMapper.selectById(order.getUid());
-        //当前用户不存在 没有上级  直接返回
-        if(ObjectUtil.isNull(userInfo) || userInfo.getSpreadUid() == 0) {
+        // 获取购买商品的用户
+        KxUser userInfo = userMapper.selectById(order.getUid());
+        // 当前用户不存在 没有上级  直接返回
+        if (ObjectUtil.isNull(userInfo) || userInfo.getSpreadUid() == 0) {
             return;
         }
 
         KxUser preUser = userMapper.selectById(userInfo.getSpreadUid());
 
-        //一级返佣金额
+        // 一级返佣金额
         BigDecimal brokeragePrice = this.computeProductBrokerage(order, LEVEL_1);
 
-        //返佣金额小于等于0 直接返回不返佣金
+        // 返佣金额小于等于0 直接返回不返佣金
 
-        if(brokeragePrice.compareTo(BigDecimal.ZERO) <= 0) {
+        if (brokeragePrice.compareTo(BigDecimal.ZERO) <= 0) {
             return;
         }
 
-        //计算上级推广员返佣之后的金额
-        double balance = NumberUtil.add(preUser.getBrokeragePrice(),brokeragePrice).doubleValue();
-        String mark = userInfo.getNickname()+"成功消费"+order.getPayPrice()+"元,奖励推广佣金"+
+        // 计算上级推广员返佣之后的金额
+        double balance = NumberUtil.add(preUser.getBrokeragePrice(), brokeragePrice).doubleValue();
+        String mark = userInfo.getNickname() + "成功消费" + order.getPayPrice() + "元,奖励推广佣金" +
                 brokeragePrice;
-        //增加流水
-        billBizService.income(userInfo.getSpreadUid(),"获得推广佣金", BillDetailEnum.CATEGORY_1.getValue(),
-                BillDetailEnum.TYPE_2.getValue(),brokeragePrice.doubleValue(),balance, mark,order.getId().toString());
+        // 增加流水
+        billBizService.income(userInfo.getSpreadUid(), "获得推广佣金", BillDetailEnum.CATEGORY_1.getValue(),
+                BillDetailEnum.TYPE_2.getValue(), brokeragePrice.doubleValue(), balance, mark, order.getId().toString());
 
-        //添加用户余额
+        // 添加用户余额
         userMapper.incBrokeragePrice(brokeragePrice, userInfo.getSpreadUid());
 
-        //一级返佣成功 跳转二级返佣
+        // 一级返佣成功 跳转二级返佣
         this.backOrderBrokerageTwo(order);
 
     }
 
     private void backOrderBrokerageTwo(KxStoreOrderVo order) {
 
-        KxUser userInfo =  userMapper.selectById(order.getUid());
+        KxUser userInfo = userMapper.selectById(order.getUid());
 
-        //获取上推广人
+        // 获取上推广人
         KxUser userInfoTwo = userMapper.selectById(userInfo.getSpreadUid());
 
-        //上推广人不存在 或者 上推广人没有上级    直接返回
-        if(ObjectUtil.isNull(userInfoTwo) || userInfoTwo.getSpreadUid() == 0) {
+        // 上推广人不存在 或者 上推广人没有上级    直接返回
+        if (ObjectUtil.isNull(userInfoTwo) || userInfoTwo.getSpreadUid() == 0) {
             return;
         }
 
 
-        //指定分销 判断 上上级是否时推广员  如果不是推广员直接返回
+        // 指定分销 判断 上上级是否时推广员  如果不是推广员直接返回
         KxUser preUser = userMapper.selectById(userInfoTwo.getSpreadUid());
 
 
-        //二级返佣金额
-        BigDecimal brokeragePrice = this.computeProductBrokerage(order,Brokerage.LEVEL_2);
+        // 二级返佣金额
+        BigDecimal brokeragePrice = this.computeProductBrokerage(order, Brokerage.LEVEL_2);
 
-        //返佣金额小于等于0 直接返回不返佣金
+        // 返佣金额小于等于0 直接返回不返佣金
         if (brokeragePrice != null && brokeragePrice.compareTo(BigDecimal.ZERO) <= 0) {
             return;
         }
 
-        //获取上上级推广员信息
-        double balance = NumberUtil.add(preUser.getBrokeragePrice(),brokeragePrice).doubleValue();
-        String mark = "二级推广人"+userInfo.getNickname()+"成功消费"+order.getPayPrice()+"元,奖励推广佣金"+
+        // 获取上上级推广员信息
+        double balance = NumberUtil.add(preUser.getBrokeragePrice(), brokeragePrice).doubleValue();
+        String mark = "二级推广人" + userInfo.getNickname() + "成功消费" + order.getPayPrice() + "元,奖励推广佣金" +
                 brokeragePrice;
 
-        //增加流水
+        // 增加流水
         if (brokeragePrice != null) {
-            billBizService.income(userInfoTwo.getSpreadUid(),"获得推广佣金",BillDetailEnum.CATEGORY_1.getValue(),
-                    BillDetailEnum.TYPE_2.getValue(),brokeragePrice.doubleValue(),balance, mark,order.getId().toString());
+            billBizService.income(userInfoTwo.getSpreadUid(), "获得推广佣金", BillDetailEnum.CATEGORY_1.getValue(),
+                    BillDetailEnum.TYPE_2.getValue(), brokeragePrice.doubleValue(), balance, mark, order.getId().toString());
         }
-        //添加用户余额
+        // 添加用户余额
         userMapper.incBrokeragePrice(brokeragePrice,
                 userInfoTwo.getSpreadUid());
     }
@@ -308,68 +304,67 @@ public class OrderBizService {
         List<KxStoreOrderProduct> storeOrderProducts = orderProductMapper.selectList(new LambdaQueryWrapper<KxStoreOrderProduct>()
                 .eq(KxStoreOrderProduct::getOrderId, order.getId()));
 
-        BigDecimal oneBrokerage = BigDecimal.ZERO;//一级返佣金额
-        BigDecimal twoBrokerage = BigDecimal.ZERO;//二级返佣金额
+        BigDecimal oneBrokerage = BigDecimal.ZERO;// 一级返佣金额
+        BigDecimal twoBrokerage = BigDecimal.ZERO;// 二级返佣金额
 
 
-        for (KxStoreOrderProduct cartInfo : storeOrderProducts){
+        for (KxStoreOrderProduct cartInfo : storeOrderProducts) {
 
             KxStoreProduct kxStoreProduct = storeProductMapper.selectById(cartInfo.getProductId());
 
-            //产品是否单独分销
-            if(ShopCommonEnum.IS_SUB_1.getValue().equals(kxStoreProduct.getIsSub())){
+            // 产品是否单独分销
+            if (ShopCommonEnum.IS_SUB_1.getValue().equals(kxStoreProduct.getIsSub())) {
 
                 KxStoreProductAttrValue storeProductAttr = storeProductAttrValueMapper.selectOne(new LambdaQueryWrapper<KxStoreProductAttrValue>()
                         .eq(KxStoreProductAttrValue::getProductId, cartInfo.getProductId()));
 
                 oneBrokerage = NumberUtil.round(NumberUtil.add(oneBrokerage,
-                                NumberUtil.mul(cartInfo.getNum(),storeProductAttr.getBrokerage()))
-                        ,2);
+                                NumberUtil.mul(cartInfo.getNum(), storeProductAttr.getBrokerage()))
+                        , 2);
 
                 twoBrokerage = NumberUtil.round(NumberUtil.add(twoBrokerage,
-                                NumberUtil.mul(cartInfo.getNum(),storeProductAttr.getBrokerageTwo()))
-                        ,2);
+                                NumberUtil.mul(cartInfo.getNum(), storeProductAttr.getBrokerageTwo()))
+                        , 2);
             }
 
         }
 
-        //获取后台一级返佣比例
+        // 获取后台一级返佣比例
         String storeBrokerageRatioStr = configService.getConfigValue(SystemConfigConstants.STORE_BROKERAGE_RATIO);
         String storeBrokerageTwoStr = configService.getConfigValue(SystemConfigConstants.STORE_BROKERAGE_TWO);
 
 
-        //一级返佣比例未设置直接返回
-        if(StrUtil.isBlank(storeBrokerageRatioStr)
-                || !NumberUtil.isNumber(storeBrokerageRatioStr)){
+        // 一级返佣比例未设置直接返回
+        if (StrUtil.isBlank(storeBrokerageRatioStr)
+                || !NumberUtil.isNumber(storeBrokerageRatioStr)) {
             return oneBrokerage;
         }
-        //二级返佣比例未设置直接返回
-        if(StrUtil.isBlank(storeBrokerageTwoStr)
-                || !NumberUtil.isNumber(storeBrokerageTwoStr)){
+        // 二级返佣比例未设置直接返回
+        if (StrUtil.isBlank(storeBrokerageTwoStr)
+                || !NumberUtil.isNumber(storeBrokerageTwoStr)) {
             return twoBrokerage;
         }
 
 
-        switch (level){
+        switch (level) {
             case LEVEL_1:
-                //根据订单获取一级返佣比例
-                BigDecimal storeBrokerageRatio = NumberUtil.round(NumberUtil.div(storeBrokerageRatioStr,"100"),2);
+                // 根据订单获取一级返佣比例
+                BigDecimal storeBrokerageRatio = NumberUtil.round(NumberUtil.div(storeBrokerageRatioStr, "100"), 2);
                 BigDecimal brokeragePrice = NumberUtil
-                        .round(NumberUtil.mul(order.getTotalPrice(),storeBrokerageRatio),2);
-                //固定返佣 + 比例返佣 = 总返佣金额
-                return NumberUtil.add(oneBrokerage,brokeragePrice);
+                        .round(NumberUtil.mul(order.getTotalPrice(), storeBrokerageRatio), 2);
+                // 固定返佣 + 比例返佣 = 总返佣金额
+                return NumberUtil.add(oneBrokerage, brokeragePrice);
             case LEVEL_2:
-                //根据订单获取一级返佣比例
-                BigDecimal storeBrokerageTwo = NumberUtil.round(NumberUtil.div(storeBrokerageTwoStr,"100"),2);
+                // 根据订单获取一级返佣比例
+                BigDecimal storeBrokerageTwo = NumberUtil.round(NumberUtil.div(storeBrokerageTwoStr, "100"), 2);
                 BigDecimal storeBrokerageTwoPrice = NumberUtil
-                        .round(NumberUtil.mul(order.getTotalPrice(),storeBrokerageTwo),2);
-                //固定返佣 + 比例返佣 = 总返佣金额
-                return NumberUtil.add(twoBrokerage,storeBrokerageTwoPrice);
+                        .round(NumberUtil.mul(order.getTotalPrice(), storeBrokerageTwo), 2);
+                // 固定返佣 + 比例返佣 = 总返佣金额
+                return NumberUtil.add(twoBrokerage, storeBrokerageTwoPrice);
             default:
         }
 
 
         return BigDecimal.ZERO;
-
     }
 }

+ 4 - 7
kxmall-system/src/main/java/com/kxmall/order/biz/OrderRiderBizService.java

@@ -1,5 +1,6 @@
 package com.kxmall.order.biz;
 
+import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.kxmall.common.enums.OrderStatusType;
 import com.kxmall.common.enums.RiderOrderStatusType;
@@ -168,21 +169,18 @@ public class OrderRiderBizService {
      * @param riderOrderStatusType 配送状态
      * @param riderId              配送员主键ID
      * @param errorMsg             配送异常【如果有】
-     * @return 是否成功
      * @throws ServiceException
      */
     @Transactional(rollbackFor = Exception.class)
-    public Boolean sendRiderMessageBusiness(String orderNo, RiderOrderStatusType riderOrderStatusType, Long riderId, String errorMsg) throws ServiceException {
-        if (!StringUtils.isEmpty(orderNo) && riderOrderStatusType != null) {
+    public void sendRiderMessageBusiness(String orderNo, RiderOrderStatusType riderOrderStatusType, Long riderId, String errorMsg) throws ServiceException {
+        if (StrUtil.isNotBlank(orderNo) && riderOrderStatusType != null) {
             RiderMessageBO riderMessageBO = new RiderMessageBO();
             riderMessageBO.setOrderNo(orderNo);
             riderMessageBO.setRiderId(riderId);
             riderMessageBO.setErrorMsg(errorMsg);
             riderMessageBO.setOrderRiderStatus(riderOrderStatusType.getCode());
             orderRiderMeaageInPut(riderMessageBO);
-            return true;
         }
-        return false;
     }
 
 
@@ -222,8 +220,7 @@ public class OrderRiderBizService {
                 }
             }
         } catch (Exception e) {
-            e.printStackTrace();
-            logger.info(e.getMessage());
+            logger.warn(e.getMessage(), e);
         }
         logger.info("【*** 订单配送消息结束,状态:{} ***】", (riderMessageBO != null) ? "success" : "fail");
     }

+ 71 - 0
kxmall-system/src/main/java/com/kxmall/order/biz/bo/SmartDispatchParamBo.java

@@ -0,0 +1,71 @@
+package com.kxmall.order.biz.bo;
+
+import com.kxmall.order.domain.KxStoreOrder;
+import lombok.Data;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 智能派单算法参数
+ * 
+ * @author kxmall
+ * @date 2025-09-07
+ */
+@Data
+@ToString
+public class SmartDispatchParamBo implements Serializable {
+
+    private static final long serialVersionUID = 9008953560090724426L;
+    
+    /**
+     * 仓库ID
+     */
+    private Long storageId;
+
+    /**
+     * 订单号
+     */
+    private String orderNo;
+
+    /**
+     * 送货地址经度
+     */
+    private BigDecimal longitude;
+
+    /**
+     * 送货地址纬度
+     */
+    private BigDecimal latitude;
+
+    /**
+     * 服务类型关键词列表
+     * 从订单商品中提取的服务类型关键词
+     */
+    private List<String> serviceKeywords;
+
+    /**
+     * 紧急程度
+     * 1-普通 2-紧急 3-特急
+     */
+    private Integer urgencyLevel;
+
+    public SmartDispatchParamBo(KxStoreOrder order, List<String> serviceKeywords) {
+        this.setOrderNo(order.getOrderId());
+        this.setLongitude(order.getLongitude());
+        this.setLatitude(order.getLatitude());
+
+        int urgencyLevel = 1;
+        if (order.getUrgentFee() != null) {
+            if (order.getUrgentFee().compareTo(new BigDecimal(10)) > 0) {
+                urgencyLevel = 3;
+            } else if (order.getUrgentFee().compareTo(new BigDecimal(0)) > 0) {
+                urgencyLevel = 2;
+            }
+        }
+        this.setUrgencyLevel(urgencyLevel);
+        this.setServiceKeywords(serviceKeywords);
+    }
+}

+ 31 - 0
kxmall-system/src/main/java/com/kxmall/order/biz/bo/package-info.java

@@ -0,0 +1,31 @@
+/**
+ * 智能派单算法业务对象包
+ * 
+ * <p>主要类说明:</p>
+ * <ul>
+ *   <li>{@link com.kxmall.order.biz.bo.SmartDispatchParamBo} - 智能派单算法参数对象,替代原来的OrderMessageBO</li>
+ * </ul>
+ * 
+ * <p>使用示例:</p>
+ * <pre>{@code
+ * // 创建派单参数
+ * SmartDispatchParamBo dispatchParam = new SmartDispatchParamBo();
+ * dispatchParam.setStorageId(1L);
+ * dispatchParam.setOrderNo("ORDER123");
+ * dispatchParam.setLatitude(new BigDecimal("39.9042"));
+ * dispatchParam.setLongitude(new BigDecimal("116.4074"));
+ * dispatchParam.setServiceKeywords(Arrays.asList("家电维修", "电器"));
+ * dispatchParam.setUrgencyLevel(2); // 紧急
+ * 
+ * // 推荐师傅
+ * List<KxRiderVo> riders = smartDispatchService.recommendRiders(dispatchParam);
+ * 
+ * // 自动选择最佳师傅
+ * KxRiderVo bestRider = smartDispatchService.selectBestRider(dispatchParam);
+ * }</pre>
+ * 
+ * @author kxmall
+ * @version 1.0
+ * @since 2025-01-08
+ */
+package com.kxmall.order.biz.bo;

+ 46 - 0
kxmall-system/src/main/java/com/kxmall/rider/domain/KxRiderExt.java

@@ -0,0 +1,46 @@
+package com.kxmall.rider.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * <p>
+ * 师傅信息扩展表
+ * </p>
+ *
+ * @author tea
+ * @since 2025-09-06
+ */
+@Getter
+@Setter
+@TableName("kx_rider_ext")
+public class KxRiderExt implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * id
+     */
+    @TableId(value = "rider_id")
+    private Long riderId;
+
+    /**
+     * 总服务数
+     */
+    private Integer currentTotalOrderCount;
+
+    /**
+     * 总评价数
+     */
+    private Integer currentTotalReviews;
+
+    /**
+     * 评价总分
+     */
+    private BigDecimal currentTotalRating;
+}

+ 2 - 0
kxmall-system/src/main/java/com/kxmall/rider/domain/vo/KxRiderVo.java

@@ -153,4 +153,6 @@ public class KxRiderVo {
     private String workTypeKeywords;
 
     private String idCardNumber;
+
+    private Double matchScore;
 }

+ 21 - 0
kxmall-system/src/main/java/com/kxmall/rider/mapper/KxRiderExtMapper.java

@@ -0,0 +1,21 @@
+package com.kxmall.rider.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.kxmall.rider.domain.KxRiderExt;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+
+/**
+ * <p>
+ * 师傅信息扩展表 Mapper 接口
+ * </p>
+ *
+ * @author tea
+ * @since 2025-09-06
+ */
+public interface KxRiderExtMapper extends BaseMapper<KxRiderExt> {
+
+    int updateStore(@Param("orderCount") int orderCount, @Param("reviewCount") int reviewCount, @Param("rating") BigDecimal rating, @Param("riderId") Long riderId);
+
+}

+ 1 - 1
kxmall-system/src/main/java/com/kxmall/rider/mapper/KxRiderMapper.java

@@ -47,5 +47,5 @@ public interface KxRiderMapper extends BaseMapperPlus<KxRiderMapper, KxRider, Kx
      */
     Integer batchUpdateWeekState(@Param("ids") List<Long> ids, @Param("workState") int workState);
 
-
+    int updateRestDayForAll();
 }

+ 4 - 0
kxmall-system/src/main/java/com/kxmall/rider/mapper/KxRiderOrderMapper.java

@@ -3,6 +3,7 @@ package com.kxmall.rider.mapper;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.toolkit.Constants;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.kxmall.common.enums.RiderOrderStatusType;
 import com.kxmall.rider.domain.KxRiderOrder;
 import com.kxmall.rider.domain.vo.KxRiderOrderVo;
 import com.kxmall.common.core.mapper.BaseMapperPlus;
@@ -11,6 +12,7 @@ import org.apache.ibatis.annotations.Param;
 import org.springframework.stereotype.Repository;
 
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
 import java.util.Date;
 import java.util.List;
 
@@ -36,4 +38,6 @@ public interface KxRiderOrderMapper extends BaseMapperPlus<KxRiderOrderMapper, K
     BigDecimal selectSumIncome(@Param("riderId") Long riderId);
 
     Page<KxRiderOrderVo> selectVoPageList(@Param("page") Page<Object> build, @Param(Constants.WRAPPER) Wrapper<KxRiderOrder> kxRiderOrderWrapper);
+
+    List<KxRiderOrder> selectFinishedOrderListByLastDay(@Param("status")RiderOrderStatusType status, @Param("time") LocalDateTime time);
 }

+ 17 - 0
kxmall-system/src/main/resources/mapper/rider/KxRiderExtMapper.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.kxmall.rider.mapper.KxRiderExtMapper">
+
+    <update id="updateStore">
+        update
+            kx_rider_ext
+        set
+            current_total_order_count = (current_total_order_count + #{orderCount}),
+            current_total_reviews     = (current_total_reviews + #{reviewCount}),
+            current_total_rating      = (current_total_rating + #{rating})
+        where
+            rider_id = #{riderId};
+    </update>
+</mapper>

+ 19 - 0
kxmall-system/src/main/resources/mapper/rider/KxRiderMapper.xml

@@ -25,5 +25,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </update>
 
+    <update id="updateRestDayForAll">
+        UPDATE kx_rider r
+        SET work_state = (
+            CASE
+                WHEN EXISTS (
+                    SELECT 1
+                        FROM kx_rider_cycle rc
+                    WHERE rc.rider_id = r.id
+                        AND
+                            rc.week_number = CASE DAYOFWEEK(CURDATE())
+                                WHEN 1 THEN 7 WHEN 2 THEN 1 WHEN 3 THEN 2 WHEN 4 THEN 3 WHEN 5 THEN 4 WHEN 6 THEN 5 WHEN 7 THEN 6
+                            END
+                    )
+                    THEN 1
+                ELSE 0
+            END
+        );
+    </update>
+
 
 </mapper>

+ 4 - 0
kxmall-system/src/main/resources/mapper/rider/KxRiderOrderMapper.xml

@@ -61,4 +61,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 left join kx_rider a on u.rider_id = a.id
             ${ew.getCustomSqlSegment}
     </select>
+
+    <select id="selectFinishedOrderListByLastDay" resultType="com.kxmall.rider.domain.KxRiderOrder">
+        select * from kx_rider_order force index (idx_status_and_finish_time) where status = #{status.code} and finish_time between DATE_SUB(#{time}, INTERVAL 1 DAY) and #{time}
+    </select>
 </mapper>

+ 163 - 0
智能派单算法说明.md

@@ -0,0 +1,163 @@
+# 智能派单算法说明
+
+## 概述
+
+本系统实现了基于服务类型匹配和距离优先级的智能派单算法,通过综合考虑多个维度来推荐最适合的师傅,提高派单效率和用户满意度。
+
+## 核心算法
+
+### 匹配度计算
+
+智能派单算法基于以下四个维度计算师傅与订单的匹配度:
+
+1. **服务类型匹配度 (40%权重)**
+   - 基于 `KxRider.workTypeKeywords` 字段
+   - 从订单商品中提取关键词
+   - 计算师傅擅长服务与订单需求的匹配程度
+
+2. **距离评分 (30%权重)**
+   - 基于 `KxRider.workRadio` 字段和师傅当前位置
+   - 计算师傅与订单地址的实际距离
+   - 距离越近分数越高
+
+3. **评分权重 (20%权重)**
+   - 基于师傅历史评价分数
+   - 从 `KxRiderExt.currentTotalRating` 获取平均评分
+
+4. **经验权重 (10%权重)**
+   - 基于师傅完成的订单数量
+   - 从 `KxRiderExt.currentTotalOrderCount` 获取经验值
+
+### 评分规则
+
+#### 服务类型匹配度
+- 完全匹配:80-100分
+- 部分匹配:40-80分
+- 无匹配:30分
+- 无设置:50分(中等分数)
+
+#### 距离评分
+- 工作半径30%内:100分
+- 工作半径60%内:85分
+- 工作半径内:70分
+- 稍超工作半径:50分
+- 远超工作半径:20分
+
+#### 评分权重
+- 5分制转100分制
+- 无评分:60分(基础分数)
+
+#### 经验权重
+- 500单以上:100分
+- 200-499单:85分
+- 100-199单:70分
+- 50-99单:55分
+- 10-49单:40分
+- 10单以下:25分
+
+## 使用方法
+
+### 1. 配置师傅信息
+
+确保师傅的以下信息已正确配置:
+
+```java
+// 服务类型关键词 (用逗号分隔)
+rider.setWorkTypeKeywords("家电维修,水电维修,开锁换锁");
+
+// 工作半径 (公里)
+rider.setWorkRadio(new BigDecimal("10.0"));
+
+// 当前位置 (纬度/经度)
+rider.setLatitude(new BigDecimal("39.9042"));
+rider.setLongitude(new BigDecimal("116.4074"));
+```
+
+### 2. 调用智能派单接口
+
+#### 获取推荐师傅列表
+```java
+@Autowired
+private IKxRiderService riderService;
+
+// 获取推荐师傅列表(按匹配度排序)
+List<KxRiderVo> recommendedRiders = riderService.getRecommendedRiders(orderMessage, storageId);
+```
+
+#### 自动选择最佳师傅
+```java
+@Autowired
+private ISmartDispatchService smartDispatchService;
+
+// 自动选择最佳师傅(匹配度≥60分)
+KxRiderVo bestRider = smartDispatchService.selectBestRider(orderMessage, storageId);
+```
+
+### 3. REST API接口
+
+```http
+POST /rider/rider/getRecommendedRiders?storageId=1
+Content-Type: application/json
+
+{
+  "orderNo": "202501080001",
+  "latitude": 39.9042,
+  "longitude": 116.4074,
+  "riderSpuBOList": [
+    {
+      "spuName": "家电维修服务"
+    }
+  ]
+}
+```
+
+## 配置项
+
+可通过配置文件调整算法参数:
+
+```yaml
+kxmall:
+  smart-dispatch:
+    enabled: true                 # 是否启用智能派单
+    service-type-weight: 0.4      # 服务类型权重
+    distance-weight: 0.3          # 距离权重  
+    rating-weight: 0.2            # 评分权重
+    experience-weight: 0.1        # 经验权重
+    min-match-score: 60.0         # 最低匹配度阈值
+    max-recommend-count: 10       # 最大推荐数量
+```
+
+## 回退机制
+
+当智能派单算法执行失败或找不到合适师傅时,系统会自动回退到原有的派单逻辑,确保系统的稳定性。
+
+## 服务类型关键词示例
+
+建议师傅设置的服务类型关键词:
+
+- **家电维修**: `家电维修,电器,空调,冰箱,洗衣机,电视`
+- **水电维修**: `水电维修,管道,电路,插座,开关,水管`
+- **开锁换锁**: `开锁换锁,锁具,门锁,密码锁`
+- **家政清洁**: `家政清洁,清洁,保洁,卫生`
+- **搬家服务**: `搬家服务,搬运,搬迁`
+
+## 性能优化
+
+1. **缓存机制**: 可考虑缓存师傅位置信息和扩展信息
+2. **异步处理**: 对于大量师傅的匹配计算可考虑异步处理
+3. **索引优化**: 为经纬度字段添加空间索引提高查询效率
+
+## 监控指标
+
+建议监控以下指标:
+- 智能派单成功率
+- 平均匹配度分数
+- 算法执行时间
+- 回退到原逻辑的比例
+
+## 扩展方向
+
+1. **机器学习优化**: 基于历史数据训练模型,提高匹配准确性
+2. **实时路况**: 集成实时交通信息,优化距离计算
+3. **用户偏好**: 考虑用户的师傅偏好历史
+4. **动态权重**: 根据时间段、订单类型动态调整权重

+ 1 - 1
需求列表.md

@@ -43,7 +43,7 @@
 - [ ] 订单排行	分析各服务类型的订单占比、用户评价关键词、师傅服务效率排名
 
 # 公众号
-- [ ] 微信模板消息:用户下单成功、师傅接单、服务完成、评价提醒;师傅新订单、派单通知、收款到账。
+- [ ] 微信模板消息:用户下单成功、师傅接单、服务完成、派单通知、
 - [ ] 站内消息:系统公告、违规提醒、活动通知。
 
 # 平台