Parcourir la source

✨ CRM:优化客户的过期到公海的逻辑

YunaiV il y a 1 an
Parent
commit
b444312ea8

+ 2 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java

@@ -45,6 +45,8 @@ public interface LogRecordConstants {
     String CRM_CUSTOMER_IMPORT_SUCCESS = "{{#isUpdate ? '导入并更新了客户【'+ #customer.name +'】' : '导入了客户【'+ #customer.name +'】'}}";
     String CRM_CUSTOMER_UPDATE_DEAL_STATUS_SUB_TYPE = "更新客户成交状态";
     String CRM_CUSTOMER_UPDATE_DEAL_STATUS_SUCCESS = "更新了客户【{{#customerName}}】的成交状态为【{{#dealStatus ? '已成交' : '未成交'}}】";
+    String CRM_CUSTOMER_FOLLOW_UP_SUB_TYPE = "客户跟进";
+    String CRM_CUSTOMER_FOLLOW_UP_SUCCESS = "客户跟进【{{#customerName}}】";
 
     // ======================= CRM_CUSTOMER_LIMIT_CONFIG 客户限制配置 =======================
 

+ 3 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java

@@ -43,6 +43,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static java.util.Collections.singletonList;
 
 @Tag(name = "管理后台 - 线索")
 @RestController
@@ -93,11 +94,11 @@ public class CrmClueController {
         return success(buildClueDetail(clue));
     }
 
-    public CrmClueRespVO buildClueDetail(CrmClueDO clue) {
+    private CrmClueRespVO buildClueDetail(CrmClueDO clue) {
         if (clue == null) {
             return null;
         }
-        return buildClueDetailList(Collections.singletonList(clue)).get(0);
+        return buildClueDetailList(singletonList(clue)).get(0);
     }
 
     @GetMapping("/page")

+ 23 - 47
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java

@@ -2,9 +2,9 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
-import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
@@ -15,7 +15,6 @@ import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
-import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerPoolConfigService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -40,13 +39,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED;
 import static java.util.Collections.singletonList;
 
 @Tag(name = "管理后台 - CRM 客户")
@@ -123,6 +120,7 @@ public class CrmCustomerController {
     @Operation(summary = "获得客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
+        customerService.autoPutCustomerPool();
         // 1. 查询客户分页
         PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO, getLoginUserId());
         if (CollUtil.isEmpty(pageResult.getList())) {
@@ -159,45 +157,21 @@ public class CrmCustomerController {
         });
     }
 
-    // TODO @芋艿:需要 review 下
-    @GetMapping("/put-in-pool-remind-page")
+    @GetMapping("/put-pool-remind-page")
     @Operation(summary = "获得待进入公海客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
-    public CommonResult<PageResult<CrmCustomerRespVO>> getPutInPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
-        // 获取公海配置 TODO @dbh52:合并到 getPutInPoolRemindCustomerPage 会更合适哈;
-        CrmCustomerPoolConfigDO poolConfigDO = customerPoolConfigService.getCustomerPoolConfig();
-        if (ObjUtil.isNull(poolConfigDO)
-                || Boolean.FALSE.equals(poolConfigDO.getEnabled())
-                || Boolean.FALSE.equals(poolConfigDO.getNotifyEnabled())) {
-            throw exception(CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED);
-        }
-
+    public CommonResult<PageResult<CrmCustomerRespVO>> getPutPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
         // 1. 查询客户分页
-        PageResult<CrmCustomerDO> pageResult = customerService.getPutInPoolRemindCustomerPage(pageVO, poolConfigDO, getLoginUserId());
-        if (CollUtil.isEmpty(pageResult.getList())) {
-            return success(PageResult.empty(pageResult.getTotal()));
-        }
-
+        PageResult<CrmCustomerDO> pageResult = customerService.getPutPoolRemindCustomerPage(pageVO, getLoginUserId());
         // 2. 拼接数据
         return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
-    @GetMapping("/put-in-pool-remind-count")
+    @GetMapping("/put-pool-remind-count")
     @Operation(summary = "获得待进入公海客户数量")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
-    public CommonResult<Long> getPutInPoolRemindCustomerCount() {
-        // 获取公海配置
-        CrmCustomerPoolConfigDO poolConfigDO = customerPoolConfigService.getCustomerPoolConfig();
-        if (ObjUtil.isNull(poolConfigDO)
-                || Boolean.FALSE.equals(poolConfigDO.getEnabled())
-                || Boolean.FALSE.equals(poolConfigDO.getNotifyEnabled())) {
-            throw exception(CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED);
-        }
-        CrmCustomerPageReqVO pageVO = new CrmCustomerPageReqVO()
-                .setPool(null)
-                .setContactStatus(CrmCustomerPageReqVO.CONTACT_TODAY)
-                .setSceneType(CrmSceneTypeEnum.OWNER.getType());
-        return success(customerService.getPutInPoolRemindCustomerCount(pageVO, poolConfigDO, getLoginUserId()));
+    public CommonResult<Long> getPutPoolRemindCustomerCount() {
+        return success(customerService.getPutPoolRemindCustomerCount(getLoginUserId()));
     }
 
     @GetMapping("/today-customer-count")
@@ -225,24 +199,26 @@ public class CrmCustomerController {
         if (poolConfig == null || !poolConfig.getEnabled()) {
             return MapUtil.empty();
         }
+        list = CollectionUtils.filterList(list, customer -> {
+            // 特殊:如果没负责人,则说明已经在公海,不用计算
+            if (customer.getOwnerUserId() == null) {
+                return false;
+            }
+            // 已成交 or 已锁定,不进入公海
+            return !customer.getDealStatus() && !customer.getLockStatus();
+        });
         return convertMap(list, CrmCustomerDO::getId, customer -> {
-            // TODO 芋艿:这样计算,貌似有点问题
             // 1.1 未成交放入公海天数
-            long dealExpireDay = 0;
-            if (!customer.getDealStatus()) {
-                dealExpireDay = poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getCreateTime());
-            }
-            if (dealExpireDay < 0) {
-                dealExpireDay = 0;
-            }
+            long dealExpireDay = poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getOwnerTime());
             // 1.2 未跟进放入公海天数
-            LocalDateTime lastTime = ObjUtil.defaultIfNull(customer.getContactLastTime(), customer.getCreateTime());
-            long contactExpireDay = poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime);
-            if (contactExpireDay < 0) {
-                contactExpireDay = 0;
+            LocalDateTime lastTime = customer.getOwnerTime();
+            if (customer.getContactLastTime() != null && customer.getContactLastTime().isAfter(lastTime)) {
+                lastTime = customer.getContactLastTime();
             }
+            long contactExpireDay = poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime);
             // 2. 返回最小的天数
-            return Math.min(dealExpireDay, contactExpireDay);
+            long poolDay = Math.min(dealExpireDay, contactExpireDay);
+            return poolDay > 0 ? poolDay : 0;
         });
     }
 

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java

@@ -57,6 +57,10 @@ public class CrmCustomerDO extends BaseDO {
      * 关联 AdminUserDO 的 id 字段
      */
     private Long ownerUserId;
+    /**
+     * 成为负责人的时间
+     */
+    private LocalDateTime ownerTime;
 
     /**
      * 锁定状态

+ 64 - 48
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java

@@ -31,40 +31,6 @@ import java.util.List;
 @Mapper
 public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
 
-    private static MPJLambdaWrapperX<CrmCustomerDO> buildPutInPoolRemindCustomerWrapper(CrmCustomerPageReqVO pageReqVO, CrmCustomerPoolConfigDO poolConfigDO, Long userId) {
-        MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
-        // 拼接数据权限的查询条件
-        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
-                CrmCustomerDO::getId, userId, pageReqVO.getSceneType(), null);
-
-        // 锁定状态不需要提醒
-        query.ne(CrmCustomerDO::getLockStatus, true);
-
-        // 情况一:未成交提醒日期区间
-        Integer dealExpireDays = poolConfigDO.getDealExpireDays();
-        LocalDateTime startDealRemindDate = LocalDateTimeUtil.beginOfDay(LocalDateTime.now())
-                .minusDays(dealExpireDays);
-        LocalDateTime endDealRemindDate = LocalDateTimeUtil.endOfDay(LocalDateTime.now())
-                .minusDays(Math.max(dealExpireDays - poolConfigDO.getNotifyDays(), 0));
-        // 情况二:未跟进提醒日期区间
-        Integer contactExpireDays = poolConfigDO.getContactExpireDays();
-        LocalDateTime startContactRemindDate = LocalDateTimeUtil.beginOfDay(LocalDateTime.now())
-                .minusDays(contactExpireDays);
-        LocalDateTime endContactRemindDate = LocalDateTimeUtil.endOfDay(LocalDateTime.now())
-                .minusDays(Math.max(contactExpireDays - poolConfigDO.getNotifyDays(), 0));
-        query
-                // 情况一:1. 未成交放入公海提醒
-                .eq(CrmCustomerDO::getDealStatus, false)
-                .between(CrmCustomerDO::getCreateTime, startDealRemindDate, endDealRemindDate)
-                // 情况二:未跟进放入公海提醒
-                .or() // 2.1 contactLastTime 为空 TODO 芋艿:这个要不要搞个默认值;
-                .isNull(CrmCustomerDO::getContactLastTime)
-                .between(CrmCustomerDO::getCreateTime, startContactRemindDate, endContactRemindDate)
-                .or() // 2.2 ContactLastTime 不为空
-                .between(CrmCustomerDO::getContactLastTime, startContactRemindDate, endContactRemindDate);
-        return query;
-    }
-
     default Long selectCountByLockStatusAndOwnerUserId(Boolean lockStatus, Long ownerUserId) {
         return selectCount(new LambdaUpdateWrapper<CrmCustomerDO>()
                 .eq(CrmCustomerDO::getLockStatus, lockStatus)
@@ -124,30 +90,80 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
         return selectJoinList(CrmCustomerDO.class, query);
     }
 
-    default List<CrmCustomerDO> selectListByLockAndNotPool(Boolean lockStatus) {
-        return selectList(new LambdaQueryWrapper<CrmCustomerDO>()
-                .eq(CrmCustomerDO::getLockStatus, lockStatus)
-                .gt(CrmCustomerDO::getOwnerUserId, 0));
-    }
-
     default CrmCustomerDO selectByCustomerName(String name) {
         return selectOne(CrmCustomerDO::getName, name);
     }
 
-    default PageResult<CrmCustomerDO> selectPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO,
-                                                                        CrmCustomerPoolConfigDO poolConfigDO,
-                                                                        Long userId) {
-        final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutInPoolRemindCustomerWrapper(pageReqVO, poolConfigDO, userId);
+    default PageResult<CrmCustomerDO> selectPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO,
+                                                                      CrmCustomerPoolConfigDO poolConfig,
+                                                                      Long userId) {
+        final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutPoolRemindCustomerQuery(pageReqVO, poolConfig, userId);
         return selectJoinPage(pageReqVO, CrmCustomerDO.class, query.selectAll(CrmCustomerDO.class));
     }
 
-    default Long selectPutInPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO,
-                                                    CrmCustomerPoolConfigDO poolConfigDO,
-                                                    Long userId) {
-        final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutInPoolRemindCustomerWrapper(pageReqVO, poolConfigDO, userId);
+    default Long selectPutPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO,
+                                                  CrmCustomerPoolConfigDO poolConfigDO,
+                                                  Long userId) {
+        final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutPoolRemindCustomerQuery(pageReqVO, poolConfigDO, userId);
         return selectCount(query);
     }
 
+    private static MPJLambdaWrapperX<CrmCustomerDO> buildPutPoolRemindCustomerQuery(CrmCustomerPageReqVO pageReqVO,
+                                                                                    CrmCustomerPoolConfigDO poolConfig,
+                                                                                    Long userId) {
+        MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
+        // 拼接数据权限的查询条件
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
+                CrmCustomerDO::getId, userId, pageReqVO.getSceneType(), null);
+
+        // 未锁定 + 未成交
+        query.eq(CrmCustomerDO::getLockStatus, false).eq(CrmCustomerDO::getDealStatus, false);
+
+        // 情况一:未成交提醒日期区间
+        Integer dealExpireDays = poolConfig.getDealExpireDays();
+        LocalDateTime startDealRemindTime = LocalDateTime.now().minusDays(dealExpireDays);
+        LocalDateTime endDealRemindTime = LocalDateTime.now()
+                .minusDays(Math.max(dealExpireDays - poolConfig.getNotifyDays(), 0));
+        // 情况二:未跟进提醒日期区间
+        Integer contactExpireDays = poolConfig.getContactExpireDays();
+        LocalDateTime startContactRemindTime = LocalDateTime.now().minusDays(contactExpireDays);
+        LocalDateTime endContactRemindTime = LocalDateTime.now()
+                .minusDays(Math.max(contactExpireDays - poolConfig.getNotifyDays(), 0));
+        query.and(q -> {
+            // 情况一:成交超时提醒
+            q.between(CrmCustomerDO::getOwnerTime, startDealRemindTime, endDealRemindTime)
+            // 情况二:跟进超时提醒
+            .or(w -> w.between(CrmCustomerDO::getOwnerTime, startContactRemindTime, endContactRemindTime)
+                    .and(p -> p.between(CrmCustomerDO::getContactLastTime, startContactRemindTime, endContactRemindTime)
+                            .or().isNull(CrmCustomerDO::getContactLastTime)));
+        });
+        return query;
+    }
+
+    /**
+     * 获得需要过期到公海的客户列表
+     *
+     * @return 客户列表
+     */
+    default List<CrmCustomerDO> selectListByAutoPool(CrmCustomerPoolConfigDO poolConfig) {
+        LambdaQueryWrapper<CrmCustomerDO> query = new LambdaQueryWrapper<>();
+        query.gt(CrmCustomerDO::getOwnerUserId, 0);
+        // 未锁定 + 未成交
+        query.eq(CrmCustomerDO::getLockStatus, false).eq(CrmCustomerDO::getDealStatus, false);
+        // 已经超时
+        LocalDateTime dealExpireTime = LocalDateTime.now().minusDays(poolConfig.getDealExpireDays());
+        LocalDateTime contactExpireTime = LocalDateTime.now().minusDays(poolConfig.getContactExpireDays());
+        query.and(q -> {
+            // 情况一:成交超时
+            q.lt(CrmCustomerDO::getOwnerTime, dealExpireTime)
+            // 情况二:跟进超时
+            .or(w -> w.lt(CrmCustomerDO::getOwnerTime, contactExpireTime)
+                    .and(p -> p.lt(CrmCustomerDO::getContactLastTime, contactExpireTime)
+                            .or().isNull(CrmCustomerDO::getContactLastTime)));
+        });
+        return selectList(query);
+    }
+
     default Long selectTodayCustomerCount(Long userId) {
         MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
         // 我负责的 + 非公海

+ 2 - 3
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java

@@ -78,8 +78,7 @@ public class CrmClueServiceImpl implements CrmClueService {
         adminUserApi.validateUser(createReqVO.getOwnerUserId());
 
         // 2. 插入线索
-        CrmClueDO clue = BeanUtils.toBean(createReqVO, CrmClueDO.class)
-                .setContactLastTime(LocalDateTime.now());
+        CrmClueDO clue = BeanUtils.toBean(createReqVO, CrmClueDO.class);
         clueMapper.insert(clue);
 
         // 3. 创建数据权限
@@ -129,7 +128,7 @@ public class CrmClueServiceImpl implements CrmClueService {
         // 校验线索是否存在
         CrmClueDO oldClue = validateClueExists(id);
 
-        // 更新
+        // 更新线索
         clueMapper.updateById(new CrmClueDO().setId(id).setFollowUpStatus(true).setContactNextTime(contactNextTime)
                 .setContactLastTime(LocalDateTime.now()).setContactLastContent(contactLastContent));
 

+ 27 - 33
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java

@@ -3,11 +3,10 @@ package cn.iocoder.yudao.module.crm.service.customer;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
 import cn.iocoder.yudao.module.crm.service.customer.bo.CrmCustomerCreateReqBO;
-import cn.iocoder.yudao.module.crm.service.followup.bo.CrmUpdateFollowUpReqBO;
 import jakarta.validation.Valid;
 
+import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -45,6 +44,15 @@ public interface CrmCustomerService {
      */
     void updateCustomerDealStatus(Long id, Boolean dealStatus);
 
+    /**
+     * 更新客户相关的跟进信息
+     *
+     * @param id 编号
+     * @param contactNextTime 下次联系时间
+     * @param contactLastContent 最后联系内容
+     */
+    void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
+
     /**
      * 删除客户
      *
@@ -88,6 +96,23 @@ public interface CrmCustomerService {
      */
     PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId);
 
+    /**
+     * 获得放入公海提醒的客户分页
+     *
+     * @param pageVO       分页查询
+     * @param userId       用户编号
+     * @return 客户分页
+     */
+    PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId);
+
+    /**
+     * 获得待进入公海的客户数量
+     *
+     * @param userId       用户编号
+     * @return 提醒数量
+     */
+    Long getPutPoolRemindCustomerCount(Long userId);
+
     /**
      * 校验客户是否存在
      *
@@ -111,13 +136,6 @@ public interface CrmCustomerService {
      */
     void lockCustomer(@Valid CrmCustomerLockReqVO lockReqVO, Long userId);
 
-    /**
-     * 更新客户相关更进信息
-     *
-     * @param customerUpdateFollowUpReqBO 请求
-     */
-    void updateCustomerFollowUp(CrmUpdateFollowUpReqBO customerUpdateFollowUpReqBO);
-
     /**
      * 创建客户
      *
@@ -161,18 +179,6 @@ public interface CrmCustomerService {
      */
     int autoPutCustomerPool();
 
-    /**
-     * 获得放入公海提醒的客户分页数据
-     *
-     * @param pageVO       分页查询
-     * @param poolConfigDO 公海配置
-     * @param userId       用户编号
-     * @return 客户分页
-     */
-    PageResult<CrmCustomerDO> getPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO,
-                                                             CrmCustomerPoolConfigDO poolConfigDO,
-                                                             Long userId);
-
     /**
      * 获得今日需联系客户数量
      *
@@ -181,18 +187,6 @@ public interface CrmCustomerService {
      */
     Long getTodayCustomerCount(Long userId);
 
-    /**
-     * 获得待进入公海的客户数量
-     *
-     * @param pageVO       分页查询
-     * @param poolConfigDO 公海配置
-     * @param userId       用户编号
-     * @return 提醒数量
-     */
-    Long getPutInPoolRemindCustomerCount(CrmCustomerPageReqVO pageVO,
-                                         CrmCustomerPoolConfigDO poolConfigDO,
-                                         Long userId);
-
     /**
      * 获得分配给我的客户数量
      *

+ 53 - 41
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java

@@ -8,7 +8,6 @@ import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
-import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
@@ -16,6 +15,7 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfi
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.permission.core.util.CrmPermissionUtils;
@@ -23,7 +23,6 @@ import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
 import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.customer.bo.CrmCustomerCreateReqBO;
-import cn.iocoder.yudao.module.crm.service.followup.bo.CrmUpdateFollowUpReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
@@ -43,7 +42,6 @@ import java.time.LocalDateTime;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
 import static cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLimitConfigTypeEnum.CUSTOMER_LOCK_LIMIT;
@@ -114,7 +112,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
      */
     private static CrmCustomerDO initCustomer(Object customer, Long ownerUserId) {
         return BeanUtils.toBean(customer, CrmCustomerDO.class).setOwnerUserId(ownerUserId)
-                .setLockStatus(false).setDealStatus(false).setContactLastTime(LocalDateTime.now());
+                .setOwnerTime(LocalDateTime.now());
     }
 
     @Override
@@ -157,6 +155,22 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         LogRecordContext.putVariable("dealStatus", dealStatus);
     }
 
+    @Override
+    @LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}",
+            success = CRM_CUSTOMER_FOLLOW_UP_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
+    public void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) {
+        // 1.1 校验存在
+        CrmCustomerDO customer = validateCustomerExists(id);
+
+        // 2. 更新客户的跟进信息
+        customerMapper.updateById(new CrmCustomerDO().setId(id).setFollowUpStatus(true).setContactNextTime(contactNextTime)
+                .setContactLastTime(LocalDateTime.now()));
+
+        // 3. 记录操作日志上下文
+        LogRecordContext.putVariable("customerName", customer.getName());
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_DELETE_SUB_TYPE, bizNo = "{{#id}}",
@@ -168,7 +182,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         // 1.2 检查引用
         validateCustomerReference(id);
 
-        // 2. 删除
+        // 2. 删除客户
         customerMapper.deleteById(id);
         // 3. 删除数据权限
         permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), id);
@@ -209,7 +223,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         permissionService.transferPermission(new CrmPermissionTransferReqBO(userId, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
                         reqVO.getId(), reqVO.getNewOwnerUserId(), reqVO.getOldOwnerPermissionLevel()));
         // 2.2 转移后重新设置负责人
-        customerMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
+        customerMapper.updateById(new CrmCustomerDO().setId(reqVO.getId())
+                .setOwnerUserId(reqVO.getNewOwnerUserId()).setOwnerTime(LocalDateTime.now()));
 
         // 3. 记录转移日志
         LogRecordContext.putVariable("customer", customer);
@@ -226,7 +241,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         if (customer.getLockStatus().equals(lockReqVO.getLockStatus())) {
             throw exception(customer.getLockStatus() ? CUSTOMER_LOCK_FAIL_IS_LOCK : CUSTOMER_UNLOCK_FAIL_IS_UNLOCK);
         }
-        // 1.3 校验锁定上限
+        // 1.3 校验锁定上限
         if (lockReqVO.getLockStatus()) {
             validateCustomerExceedLockLimit(userId);
         }
@@ -239,11 +254,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         LogRecordContext.putVariable("customer", customer);
     }
 
-    @Override
-    public void updateCustomerFollowUp(CrmUpdateFollowUpReqBO customerUpdateFollowUpReqBO) {
-        customerMapper.updateById(BeanUtils.toBean(customerUpdateFollowUpReqBO, CrmCustomerDO.class).setId(customerUpdateFollowUpReqBO.getBizId()));
-    }
-
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_CREATE_SUB_TYPE, bizNo = "{{#customer.id}}",
@@ -263,7 +273,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     @Override
-    public CrmCustomerImportRespVO importCustomerList(List<CrmCustomerImportExcelVO> importCustomers, CrmCustomerImportReqVO importReqVO) {
+    public CrmCustomerImportRespVO importCustomerList(List<CrmCustomerImportExcelVO> importCustomers,
+                                                      CrmCustomerImportReqVO importReqVO) {
         if (CollUtil.isEmpty(importCustomers)) {
             throw exception(CUSTOMER_IMPORT_LIST_IS_EMPTY);
         }
@@ -383,12 +394,13 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         // 1.4  校验负责人是否到达上限
         validateCustomerExceedOwnerLimit(ownerUserId, customers.size());
 
-        // 2.1 领取公海数据
+        // 2. 领取公海数据
         List<CrmCustomerDO> updateCustomers = new ArrayList<>();
         List<CrmPermissionCreateReqBO> createPermissions = new ArrayList<>();
         customers.forEach(customer -> {
             // 2.1. 设置负责人
-            updateCustomers.add(new CrmCustomerDO().setId(customer.getId()).setOwnerUserId(ownerUserId));
+            updateCustomers.add(new CrmCustomerDO().setId(customer.getId())
+                    .setOwnerUserId(ownerUserId).setOwnerTime(LocalDateTime.now()));
             // 2.2. 创建负责人数据权限
             createPermissions.add(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
                     .setBizId(customer.getId()).setUserId(ownerUserId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
@@ -415,34 +427,23 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         if (poolConfig == null || !poolConfig.getEnabled()) {
             return 0;
         }
-        // 1.1 获取没有锁定的不在公海的客户
-        List<CrmCustomerDO> customerList = customerMapper.selectListByLockAndNotPool(Boolean.FALSE);
-        // TODO @puhui999:下面也搞到 sql 里去哈;写 or 查询,问题不大的;低 393 到 402;原因是,避免无用的太多数据查询到 java 进程里;
-        List<CrmCustomerDO> poolCustomerList = new ArrayList<>();
-        poolCustomerList.addAll(filterList(customerList, customer ->
-                !customer.getDealStatus() && (poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getCreateTime())) <= 0));
-        poolCustomerList.addAll(filterList(customerList, customer -> {
-            if (!customer.getDealStatus()) { // 这里只处理成交的
-                return false;
-            }
-            LocalDateTime lastTime = ObjUtil.defaultIfNull(customer.getContactLastTime(), customer.getCreateTime());
-            return (poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime)) <= 0;
-        }));
-
+        // 1. 获得需要放到的客户列表
+        List<CrmCustomerDO> customerList = customerMapper.selectListByAutoPool(poolConfig);
         // 2. 逐个放入公海
         int count = 0;
-        for (CrmCustomerDO customer : poolCustomerList) {
+        for (CrmCustomerDO customer : customerList) {
             try {
                 getSelf().putCustomerPool(customer);
                 count++;
             } catch (Throwable e) {
-                log.error("[autoPutCustomerPool][Customer 客户({}) 放入公海异常]", customer.getId(), e);
+                log.error("[autoPutCustomerPool][客户({}) 放入公海异常]", customer.getId(), e);
             }
         }
         return count;
     }
 
-    private void putCustomerPool(CrmCustomerDO customer) {
+    @Transactional // 需要 protected 修饰,因为需要在事务中调用
+    protected void putCustomerPool(CrmCustomerDO customer) {
         // 1. 设置负责人为 NULL
         int updateOwnerUserIncr = customerMapper.updateOwnerUserIdById(customer.getId(), null);
         if (updateOwnerUserIncr == 0) {
@@ -486,17 +487,29 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     @Override
-    public PageResult<CrmCustomerDO> getPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO,
-                                                                    CrmCustomerPoolConfigDO poolConfigDO,
-                                                                    Long userId) {
-        return customerMapper.selectPutInPoolRemindCustomerPage(pageReqVO, poolConfigDO, userId);
+    public PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId) {
+        CrmCustomerPoolConfigDO poolConfig = customerPoolConfigService.getCustomerPoolConfig();
+        if (ObjUtil.isNull(poolConfig)
+                || Boolean.FALSE.equals(poolConfig.getEnabled())
+                || Boolean.FALSE.equals(poolConfig.getNotifyEnabled())) {
+            return PageResult.empty();
+        }
+        return customerMapper.selectPutPoolRemindCustomerPage(pageVO, poolConfig, userId);
     }
 
     @Override
-    public Long getPutInPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO,
-                                                CrmCustomerPoolConfigDO poolConfigDO,
-                                                Long userId) {
-        return customerMapper.selectPutInPoolRemindCustomerCount(pageReqVO, poolConfigDO, userId);
+    public Long getPutPoolRemindCustomerCount(Long userId) {
+        CrmCustomerPoolConfigDO poolConfig = customerPoolConfigService.getCustomerPoolConfig();
+        if (ObjUtil.isNull(poolConfig)
+                || Boolean.FALSE.equals(poolConfig.getEnabled())
+                || Boolean.FALSE.equals(poolConfig.getNotifyEnabled())) {
+            return 0L;
+        }
+        CrmCustomerPageReqVO pageVO = new CrmCustomerPageReqVO()
+                .setPool(null)
+                .setContactStatus(CrmCustomerPageReqVO.CONTACT_TODAY)
+                .setSceneType(CrmSceneTypeEnum.OWNER.getType());
+        return customerMapper.selectPutPoolRemindCustomerCount(pageVO, poolConfig, userId);
     }
 
     @Override
@@ -596,7 +609,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         }
     }
 
-
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *

+ 9 - 9
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java

@@ -72,24 +72,24 @@ public class CrmFollowUpRecordServiceImpl implements CrmFollowUpRecordService {
         CrmFollowUpRecordDO followUpRecord = BeanUtils.toBean(createReqVO, CrmFollowUpRecordDO.class);
         crmFollowUpRecordMapper.insert(followUpRecord);
 
-        LocalDateTime now = LocalDateTime.now();
-        CrmUpdateFollowUpReqBO updateFollowUpReqBO = new CrmUpdateFollowUpReqBO().setBizId(followUpRecord.getBizId())
-                .setContactLastTime(now).setContactNextTime(followUpRecord.getNextTime()).setContactLastContent(followUpRecord.getContent());
         // 2. 更新 bizId 对应的记录;
-        if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_BUSINESS.getType(), followUpRecord.getBizType())) { // 更新商机跟进信息
+        CrmUpdateFollowUpReqBO updateFollowUpReqBO = new CrmUpdateFollowUpReqBO().setBizId(followUpRecord.getBizId())
+                .setContactLastTime(LocalDateTime.now())
+                .setContactNextTime(followUpRecord.getNextTime()).setContactLastContent(followUpRecord.getContent());
+        if (ObjUtil.equal(CrmBizTypeEnum.CRM_BUSINESS.getType(), followUpRecord.getBizType())) { // 更新商机跟进信息
             businessService.updateBusinessFollowUpBatch(Collections.singletonList(updateFollowUpReqBO));
         }
-        if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CLUE.getType(), followUpRecord.getBizType())) { // 更新线索跟进信息
+        if (ObjUtil.equal(CrmBizTypeEnum.CRM_CLUE.getType(), followUpRecord.getBizType())) { // 更新线索跟进信息
             clueService.updateClueFollowUp(followUpRecord.getId(), followUpRecord.getNextTime(), followUpRecord.getContent());
         }
-        if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CONTACT.getType(), followUpRecord.getBizType())) { // 更新联系人跟进信息
+        if (ObjUtil.equal(CrmBizTypeEnum.CRM_CONTACT.getType(), followUpRecord.getBizType())) { // 更新联系人跟进信息
             contactService.updateContactFollowUpBatch(Collections.singletonList(updateFollowUpReqBO));
         }
-        if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CONTRACT.getType(), followUpRecord.getBizType())) { // 更新合同跟进信息
+        if (ObjUtil.equal(CrmBizTypeEnum.CRM_CONTRACT.getType(), followUpRecord.getBizType())) { // 更新合同跟进信息
             contractService.updateContractFollowUp(updateFollowUpReqBO);
         }
-        if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CUSTOMER.getType(), followUpRecord.getBizType())) { // 更新客户跟进信息
-            customerService.updateCustomerFollowUp(updateFollowUpReqBO);
+        if (ObjUtil.equal(CrmBizTypeEnum.CRM_CUSTOMER.getType(), followUpRecord.getBizType())) { // 更新客户跟进信息
+            customerService.updateCustomerFollowUp(followUpRecord.getBizId(), followUpRecord.getNextTime(), followUpRecord.getContent());
         }
 
         // 3.1 更新 contactIds 对应的记录,不更新 lastTime 和 lastContent