Ver código fonte

feat(queue): 实现客户退回公海自动处理功能

- 修改生产和测试环境redis数据库配置,统一调整db编号
- 新增AUTO_RECORD_ORDER_DATA队列常量,用于退回公海的消息处理
- 新增ReturnPublicConsumer消费者,监听自动退回队列实现客户无跟进时退回公海逻辑
- 在SysCustomerDataMapper中更新退回公海SQL,添加reset_datetime字段更新时间戳
- 在SysCustomerDataService.claimCustomer方法中增加客户持有上限校验逻辑,限制非管理员用户认领数量
- 设置自动退回公海的延迟队列任务,延时基于参数配置的天数转换为秒
- 增加每日手动退回公海上限检查,使用Redis记录并限制当日退回次数
- 修正SysCustomerFollowService中的延时任务时间计算,避免取绝对值导致时间错误
- 为SysParamEntity的关键参数字段添加MyBatis更新策略,确保字段可覆盖更新
zhangxf 4 semanas atrás
pai
commit
4ece68686a

+ 1 - 0
src/main/java/com/jtzx/crm/module/sys/common/QueueCommon.java

@@ -6,5 +6,6 @@ public class QueueCommon {
      * 任务开始队列
      */
     public static  final String FOLLOW_ORDER_DATA = "followOrderData";
+    public static  final String AUTO_RECORD_ORDER_DATA = "autoRecordOrderData";
 
 }

+ 4 - 3
src/main/java/com/jtzx/crm/module/sys/entity/SysParamEntity.java

@@ -1,6 +1,7 @@
 package com.jtzx.crm.module.sys.entity;
 
 import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
 import io.swagger.v3.oas.annotations.media.Schema;
@@ -29,15 +30,15 @@ public class SysParamEntity extends HelioBaseEntity<Long> {
     private String title;
 
     @Schema(description = "持有上限")
-    @TableField(value = "hold_top_count")
+    @TableField(value = "hold_top_count",updateStrategy = FieldStrategy.ALWAYS)
     private Integer holdTopCount;
 
     @Schema(description = "日退上限")
-    @TableField(value = "day_back_top_count")
+    @TableField(value = "day_back_top_count",updateStrategy = FieldStrategy.ALWAYS)
     private Integer dayBackTopCount;
 
     @Schema(description = "退回公海天数")
-    @TableField(value = "back_public_day_count")
+    @TableField(value = "back_public_day_count",updateStrategy = FieldStrategy.ALWAYS)
     private Integer backPublicDayCount;
 
 }

+ 2 - 1
src/main/java/com/jtzx/crm/module/sys/mapper/xml/SysCustomerDataMapper.xml

@@ -10,7 +10,8 @@
             claim_user_name = null,
             claim_datetime = null,
             claim_user_group_id = null,
-            claim_status = 0
+            claim_status = 0,
+            reset_datetime = NOW()
         where id = #{customerId}
     </update>
 

+ 81 - 0
src/main/java/com/jtzx/crm/module/sys/queue/ReturnPublicConsumer.java

@@ -0,0 +1,81 @@
+package com.jtzx.crm.module.sys.queue;
+
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.jtzx.crm.module.sys.common.QueueCommon;
+import com.jtzx.crm.module.sys.entity.SysCustomerDataEntity;
+import com.jtzx.crm.module.sys.entity.SysCustomerFollowEntity;
+import com.jtzx.crm.module.sys.entity.SysCustomerOperLogEntity;
+import com.jtzx.crm.module.sys.mapper.SysCustomerDataMapper;
+import com.jtzx.crm.module.sys.mapper.SysCustomerFollowMapper;
+import com.jtzx.crm.module.sys.mapper.SysCustomerOperLogMapper;
+import com.redismq.RedisListener;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * 退回公海
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ReturnPublicConsumer {
+
+
+    private final SysCustomerDataMapper sysCustomerDataMapper;
+
+    private final SysCustomerFollowMapper sysCustomerFollowMapper;
+
+    private final SysCustomerOperLogMapper sysCustomerOperLogMapper;
+
+    /**
+     * 定时处理退回公海
+     */
+    @RedisListener(queue = QueueCommon.AUTO_RECORD_ORDER_DATA, delay = true)
+    public void handleReturnPublicConsumer(String data) {
+        //将json数据转换为对象,数据来源为Redis队列
+        SysCustomerDataEntity sysCustomerDataEntity = JSONUtil.toBean(data, SysCustomerDataEntity.class);
+        Long id = sysCustomerDataEntity.getId();
+        Long claimUserId = sysCustomerDataEntity.getClaimUserId();
+        SysCustomerDataEntity customerData = sysCustomerDataMapper.selectById(id);
+        if (customerData != null && customerData.getClaimUserId()!=null && customerData.getClaimUserId().equals(claimUserId)) {
+            long count =
+                    sysCustomerFollowMapper.selectCount(
+                            new LambdaQueryWrapper<>(SysCustomerFollowEntity.class)
+                                    .select(
+                                            SysCustomerFollowEntity::getId
+                                    )
+                                    .eq(SysCustomerFollowEntity::getCustomerId, id)
+                                    .eq(SysCustomerFollowEntity::getClaimUserId, claimUserId)
+                    );
+            if ((int) count == 0) {
+                // 无跟进记录, 退回公海
+                sysCustomerDataMapper.returnCustomerData(id);
+                //添加认领日志
+                SysCustomerOperLogEntity logEntity = SysCustomerOperLogEntity.builder()
+                        .customerId(sysCustomerDataEntity.getId())
+                        .customerName(sysCustomerDataEntity.getName())
+//                        .operUserId(UserContextHolder.getUserId())
+                        .operName("系统")
+                        .operAction(3)
+                        .operDatetime(LocalDateTime.now())
+                        .provinceId(sysCustomerDataEntity.getProvinceId())
+                        .provinceName(sysCustomerDataEntity.getProvinceName())
+                        .cityId(sysCustomerDataEntity.getCityId())
+                        .cityName(sysCustomerDataEntity.getCityName())
+                        .content("用户" + sysCustomerDataEntity.getClaimUserName() + "无跟进,系统自动将【" + sysCustomerDataEntity.getName() + "】转至公海")
+                        .operUserGroupId(sysCustomerDataEntity.getClaimUserGroupId())
+                        .operDataType(0)
+                        .build();
+                sysCustomerOperLogMapper.insert(logEntity);
+            }
+        }
+
+
+    }
+
+}

+ 87 - 1
src/main/java/com/jtzx/crm/module/sys/service/SysCustomerDataService.java

@@ -4,23 +4,32 @@ import cc.uncarbon.framework.core.context.UserContextHolder;
 import cc.uncarbon.framework.core.exception.BusinessException;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.google.common.collect.Lists;
+import com.jtzx.crm.module.sys.common.QueueCommon;
 import com.jtzx.crm.module.sys.entity.*;
 import com.jtzx.crm.module.sys.mapper.*;
 import com.jtzx.crm.module.sys.model.request.AdminSysCustomerDataInsertOrUpdateDTO;
 import com.jtzx.crm.module.sys.model.request.TransferUserReqDTO;
 import com.jtzx.crm.module.sys.model.response.SysCustomerDataBO;
 import com.jtzx.crm.module.sys.model.response.TransferUserReqBO;
+import com.redismq.utils.RedisMQTemplate;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.time.Duration;
 import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 
 /**
@@ -43,6 +52,11 @@ public class SysCustomerDataService {
 
     private final SysUserMapper sysUserMapper;
 
+    private final SysParamMapper sysParamMapper;
+
+    private final RedisMQTemplate redisMQTemplate;
+
+    private final RedisTemplate<String, Object> stringSetRedisTemplate;
 
     /**
      * 根据 ID 取详情
@@ -203,7 +217,33 @@ public class SysCustomerDataService {
      * @param customerId
      */
     @Transactional(rollbackFor = Exception.class)
-    public void claimCustomer(Long customerId) {
+    public void claimCustomer(Long customerId) throws BusinessException {
+
+        SysParamEntity sysParamEntity =
+                sysParamMapper.selectOne(
+                        new LambdaQueryWrapper<>(SysParamEntity.class)
+                );
+        if (sysParamEntity == null) {
+            throw new BusinessException(400, "参数配置未设置");
+        }
+        // 客户持有上限判断
+        if(!UserContextHolder.getUserContext().getRoles().contains("Admin")){
+            if (sysParamEntity.getHoldTopCount() != null && sysParamEntity.getHoldTopCount() > 0) {
+                long customerCount =
+                        sysCustomerDataMapper.selectCount(
+                                new LambdaQueryWrapper<>(SysCustomerDataEntity.class)
+                                        .select(
+                                                SysCustomerDataEntity::getId
+                                        )
+                                        .eq(SysCustomerDataEntity::getClaimUserId, UserContextHolder.getUserId())
+                        );
+                if (customerCount > 0 && customerCount > sysParamEntity.getHoldTopCount()) {
+                    throw new BusinessException(400, "认领数量已超过设定的持有数量上限,不能再进行认领操作");
+                }
+            }
+        }
+
+
         SysCustomerDataEntity sysCustomerDataEntity = sysCustomerDataMapper.selectById(customerId);
         if (sysCustomerDataEntity == null) {
             throw new BusinessException(400, "客户不存在");
@@ -250,6 +290,21 @@ public class SysCustomerDataService {
                 .operDataType(0)
                 .build();
         sysCustomerOperLogMapper.insert(logEntity);
+
+        //添加自动退回定时任务
+        if (sysParamEntity.getBackPublicDayCount() != null && sysParamEntity.getBackPublicDayCount() > 0) {
+            Integer backPublicDayCount = sysParamEntity.getBackPublicDayCount();//单位为 天
+            DateTime date = DateUtil.date();
+            //结束的时间
+            DateTime dateTime = DateUtil.offsetDay(date, backPublicDayCount);
+            //计算结束时间,单位 秒
+            long between = ChronoUnit.SECONDS.between(DateUtil.toLocalDateTime(date), DateUtil.toLocalDateTime(dateTime));
+            if (between > 0) {
+                // 将对象转换为 json 数据
+                String data = JSONUtil.toJsonStr(sysCustomerDataEntity);
+                redisMQTemplate.sendDelayMessage(data, QueueCommon.AUTO_RECORD_ORDER_DATA, Duration.ofSeconds(between));
+            }
+        }
     }
 
 
@@ -273,6 +328,37 @@ public class SysCustomerDataService {
                 throw new BusinessException(400, "认领人不正确,无权操作");
             }
         }
+        // 每日手动退回公海上限判断
+        if(!UserContextHolder.getUserContext().getRoles().contains("Admin")){
+            SysParamEntity sysParamEntity =
+                    sysParamMapper.selectOne(
+                            new LambdaQueryWrapper<>(SysParamEntity.class)
+                    );
+            if (sysParamEntity == null) {
+                throw new BusinessException(400, "参数配置未设置");
+            }
+            if (sysParamEntity.getDayBackTopCount() != null && sysParamEntity.getDayBackTopCount() > 0) {
+                String key = "return_customer_data_" + sysCustomerDataEntity.getClaimUserId();
+                //先从redis中获取当日退回公海次数
+                Object count = stringSetRedisTemplate.opsForValue().get(key);
+                if (count == null) {
+                    DateTime date = DateUtil.date();
+                    DateTime dateTime = DateUtil.endOfDay(date);//当日的结束时间
+                    //计算结束时间,单位 秒
+                    long between = ChronoUnit.SECONDS.between(DateUtil.toLocalDateTime(date), DateUtil.toLocalDateTime(dateTime));
+                    if (between > 0) {
+                        stringSetRedisTemplate.opsForValue().set(key, 1, between, TimeUnit.SECONDS);
+                    }
+                } else {
+                    if ((Integer) count <= sysParamEntity.getDayBackTopCount()) {
+                        stringSetRedisTemplate.opsForValue().increment(key);
+                    }else {
+                        throw new BusinessException(400, "操作失败,已超过每日手动退回公海上限");
+                    }
+                }
+            }
+        }
+
         sysCustomerDataMapper.returnCustomerData(customerId);
         //添加认领日志
         SysCustomerOperLogEntity logEntity = SysCustomerOperLogEntity.builder()

+ 1 - 1
src/main/java/com/jtzx/crm/module/sys/service/SysCustomerFollowService.java

@@ -96,7 +96,7 @@ public class SysCustomerFollowService {
             LocalDateTime now = LocalDateTime.now();
             if (dto.getOrderDatetime().isAfter(now)) {
                 //发送定时任务,时间到之后改为已结束
-                long between = Math.abs(ChronoUnit.SECONDS.between(now, dto.getOrderDatetime()));
+                long between = ChronoUnit.SECONDS.between(now, dto.getOrderDatetime());
                 if (between > 0) {
                     redisMQTemplate.sendDelayMessage(entity.getId(), QueueCommon.FOLLOW_ORDER_DATA, Duration.ofSeconds(between));
                 }

+ 2 - 2
src/main/resources/application-prod.yml

@@ -29,13 +29,13 @@ spring:
       # Redis登录密码;如果没有设置密码,请注释掉下面这行,避免向 Redis-server 发送 AUTH 鉴权请求,引发程序启动失败
       password: VmANNzPX8EQZ3x01oM22
       # Redis数据库序号
-      database: 14
+      database: 4
   redismq: #消息队列
     namespace: jtzx-crm #命名空间,同一个redis环境隔离
     client: #redis 客户端配置
       client-name: crmRedisMQ
       host: ${spring.data.redis.host:192.168.1.70}
-      database: 14
+      database: 4
       port: ${spring.data.redis.port:6379}
       password: ${spring.data.redis.password:VmANNzPX8EQZ3x01oM22}
     global-config: #全局配置

+ 2 - 2
src/main/resources/application-test.yml

@@ -29,13 +29,13 @@ spring:
       # Redis登录密码;如果没有设置密码,请注释掉下面这行,避免向 Redis-server 发送 AUTH 鉴权请求,引发程序启动失败
       password: VmANNzPX8EQZ3x01oM22
       # Redis数据库序号
-      database: 14
+      database: 12
   redismq: #消息队列
     namespace: jtzx-crm #命名空间,同一个redis环境隔离
     client: #redis 客户端配置
       client-name: crmRedisMQ
       host: ${spring.data.redis.host:172.28.189.252}
-      database: 14
+      database: 12
       port: ${spring.data.redis.port:6379}
       password: ${spring.data.redis.password:VmANNzPX8EQZ3x01oM22}
     global-config: #全局配置