From b444312ea888baa950a829c1105445ab463a9eaf Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 20 Feb 2024 19:07:48 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20CRM=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=9A=84=E8=BF=87=E6=9C=9F=E5=88=B0=E5=85=AC?= =?UTF-8?q?=E6=B5=B7=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/crm/enums/LogRecordConstants.java | 2 + .../admin/clue/CrmClueController.java | 5 +- .../admin/customer/CrmCustomerController.java | 70 ++++------- .../dataobject/customer/CrmCustomerDO.java | 4 + .../dal/mysql/customer/CrmCustomerMapper.java | 112 ++++++++++-------- .../crm/service/clue/CrmClueServiceImpl.java | 5 +- .../service/customer/CrmCustomerService.java | 60 +++++----- .../customer/CrmCustomerServiceImpl.java | 94 ++++++++------- .../CrmFollowUpRecordServiceImpl.java | 18 +-- 9 files changed, 187 insertions(+), 183 deletions(-) diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java index 7301cf8b1..0299a8d31 100644 --- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java +++ b/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 客户限制配置 ======================= diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java index 044b39d34..ecfef7d01 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java +++ b/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") diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java index bf25238a2..034cb5ffc 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java +++ b/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> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) { + customerService.autoPutCustomerPool(); // 1. 查询客户分页 PageResult 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> 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> getPutPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) { // 1. 查询客户分页 - PageResult pageResult = customerService.getPutInPoolRemindCustomerPage(pageVO, poolConfigDO, getLoginUserId()); - if (CollUtil.isEmpty(pageResult.getList())) { - return success(PageResult.empty(pageResult.getTotal())); - } - + PageResult 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 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 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; }); } diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java index e59311d0a..76d511115 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java +++ b/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; /** * 锁定状态 diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java index 95478df9d..c70d14900 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java +++ b/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 { - private static MPJLambdaWrapperX buildPutInPoolRemindCustomerWrapper(CrmCustomerPageReqVO pageReqVO, CrmCustomerPoolConfigDO poolConfigDO, Long userId) { - MPJLambdaWrapperX 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() .eq(CrmCustomerDO::getLockStatus, lockStatus) @@ -124,30 +90,80 @@ public interface CrmCustomerMapper extends BaseMapperX { return selectJoinList(CrmCustomerDO.class, query); } - default List selectListByLockAndNotPool(Boolean lockStatus) { - return selectList(new LambdaQueryWrapper() - .eq(CrmCustomerDO::getLockStatus, lockStatus) - .gt(CrmCustomerDO::getOwnerUserId, 0)); - } - default CrmCustomerDO selectByCustomerName(String name) { return selectOne(CrmCustomerDO::getName, name); } - default PageResult selectPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO, - CrmCustomerPoolConfigDO poolConfigDO, - Long userId) { - final MPJLambdaWrapperX query = buildPutInPoolRemindCustomerWrapper(pageReqVO, poolConfigDO, userId); + default PageResult selectPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO, + CrmCustomerPoolConfigDO poolConfig, + Long userId) { + final MPJLambdaWrapperX 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 query = buildPutInPoolRemindCustomerWrapper(pageReqVO, poolConfigDO, userId); + default Long selectPutPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO, + CrmCustomerPoolConfigDO poolConfigDO, + Long userId) { + final MPJLambdaWrapperX query = buildPutPoolRemindCustomerQuery(pageReqVO, poolConfigDO, userId); return selectCount(query); } + private static MPJLambdaWrapperX buildPutPoolRemindCustomerQuery(CrmCustomerPageReqVO pageReqVO, + CrmCustomerPoolConfigDO poolConfig, + Long userId) { + MPJLambdaWrapperX 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 selectListByAutoPool(CrmCustomerPoolConfigDO poolConfig) { + LambdaQueryWrapper 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 query = new MPJLambdaWrapperX<>(); // 我负责的 + 非公海 diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java index fe10a133d..7251eee26 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java +++ b/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)); diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java index 8d7f47859..f4858ce53 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java +++ b/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 getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId); + /** + * 获得放入公海提醒的客户分页 + * + * @param pageVO 分页查询 + * @param userId 用户编号 + * @return 客户分页 + */ + PageResult 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 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); - /** * 获得分配给我的客户数量 * diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java index ce88a89bc..37d94abe8 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java +++ b/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 importCustomers, CrmCustomerImportReqVO importReqVO) { + public CrmCustomerImportRespVO importCustomerList(List 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 updateCustomers = new ArrayList<>(); List 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 customerList = customerMapper.selectListByLockAndNotPool(Boolean.FALSE); - // TODO @puhui999:下面也搞到 sql 里去哈;写 or 查询,问题不大的;低 393 到 402;原因是,避免无用的太多数据查询到 java 进程里; - List 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 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 getPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO, - CrmCustomerPoolConfigDO poolConfigDO, - Long userId) { - return customerMapper.selectPutInPoolRemindCustomerPage(pageReqVO, poolConfigDO, userId); + public PageResult 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 生效问题 * diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java index 5cfdbb1dc..00242b19c 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java +++ b/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