mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-11-01 02:38:43 +08:00 
			
		
		
		
	✨ CRM:优化客户的过期到公海的逻辑
This commit is contained in:
		| @@ -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") | ||||
|   | ||||
| @@ -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; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -57,6 +57,10 @@ public class CrmCustomerDO extends BaseDO { | ||||
|      * 关联 AdminUserDO 的 id 字段 | ||||
|      */ | ||||
|     private Long ownerUserId; | ||||
|     /** | ||||
|      * 成为负责人的时间 | ||||
|      */ | ||||
|     private LocalDateTime ownerTime; | ||||
|  | ||||
|     /** | ||||
|      * 锁定状态 | ||||
|   | ||||
| @@ -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<>(); | ||||
|         // 我负责的 + 非公海 | ||||
|   | ||||
| @@ -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)); | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|     /** | ||||
|      * 获得分配给我的客户数量 | ||||
|      * | ||||
|   | ||||
| @@ -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 生效问题 | ||||
|      * | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 YunaiV
					YunaiV