Commit b444312ea888baa950a829c1105445ab463a9eaf

Authored by YunaiV
1 parent 3e652493

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

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 {
45 45 String CRM_CUSTOMER_IMPORT_SUCCESS = "{{#isUpdate ? '导入并更新了客户【'+ #customer.name +'】' : '导入了客户【'+ #customer.name +'】'}}";
46 46 String CRM_CUSTOMER_UPDATE_DEAL_STATUS_SUB_TYPE = "更新客户成交状态";
47 47 String CRM_CUSTOMER_UPDATE_DEAL_STATUS_SUCCESS = "更新了客户【{{#customerName}}】的成交状态为【{{#dealStatus ? '已成交' : '未成交'}}】";
  48 + String CRM_CUSTOMER_FOLLOW_UP_SUB_TYPE = "客户跟进";
  49 + String CRM_CUSTOMER_FOLLOW_UP_SUCCESS = "客户跟进【{{#customerName}}】";
48 50  
49 51 // ======================= CRM_CUSTOMER_LIMIT_CONFIG 客户限制配置 =======================
50 52  
... ...
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.
43 43 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
44 44 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
45 45 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  46 +import static java.util.Collections.singletonList;
46 47  
47 48 @Tag(name = "管理后台 - 线索")
48 49 @RestController
... ... @@ -93,11 +94,11 @@ public class CrmClueController {
93 94 return success(buildClueDetail(clue));
94 95 }
95 96  
96   - public CrmClueRespVO buildClueDetail(CrmClueDO clue) {
  97 + private CrmClueRespVO buildClueDetail(CrmClueDO clue) {
97 98 if (clue == null) {
98 99 return null;
99 100 }
100   - return buildClueDetailList(Collections.singletonList(clue)).get(0);
  101 + return buildClueDetailList(singletonList(clue)).get(0);
101 102 }
102 103  
103 104 @GetMapping("/page")
... ...
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;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.util.ObjUtil;
6 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
8 8 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
9 9 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
10 10 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
... ... @@ -15,7 +15,6 @@ import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
15 15 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
16 16 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
17 17 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
18   -import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
19 18 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerPoolConfigService;
20 19 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
21 20 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
... ... @@ -40,13 +39,11 @@ import java.util.List;
40 39 import java.util.Map;
41 40 import java.util.stream.Stream;
42 41  
43   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
44 42 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
45 43 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
46 44 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
47 45 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
48 46 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
49   -import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED;
50 47 import static java.util.Collections.singletonList;
51 48  
52 49 @Tag(name = "管理后台 - CRM 客户")
... ... @@ -123,6 +120,7 @@ public class CrmCustomerController {
123 120 @Operation(summary = "获得客户分页")
124 121 @PreAuthorize("@ss.hasPermission('crm:customer:query')")
125 122 public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
  123 + customerService.autoPutCustomerPool();
126 124 // 1. 查询客户分页
127 125 PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO, getLoginUserId());
128 126 if (CollUtil.isEmpty(pageResult.getList())) {
... ... @@ -159,45 +157,21 @@ public class CrmCustomerController {
159 157 });
160 158 }
161 159  
162   - // TODO @芋艿:需要 review 下
163   - @GetMapping("/put-in-pool-remind-page")
  160 + @GetMapping("/put-pool-remind-page")
164 161 @Operation(summary = "获得待进入公海客户分页")
165 162 @PreAuthorize("@ss.hasPermission('crm:customer:query')")
166   - public CommonResult<PageResult<CrmCustomerRespVO>> getPutInPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
167   - // 获取公海配置 TODO @dbh52:合并到 getPutInPoolRemindCustomerPage 会更合适哈;
168   - CrmCustomerPoolConfigDO poolConfigDO = customerPoolConfigService.getCustomerPoolConfig();
169   - if (ObjUtil.isNull(poolConfigDO)
170   - || Boolean.FALSE.equals(poolConfigDO.getEnabled())
171   - || Boolean.FALSE.equals(poolConfigDO.getNotifyEnabled())) {
172   - throw exception(CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED);
173   - }
174   -
  163 + public CommonResult<PageResult<CrmCustomerRespVO>> getPutPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
175 164 // 1. 查询客户分页
176   - PageResult<CrmCustomerDO> pageResult = customerService.getPutInPoolRemindCustomerPage(pageVO, poolConfigDO, getLoginUserId());
177   - if (CollUtil.isEmpty(pageResult.getList())) {
178   - return success(PageResult.empty(pageResult.getTotal()));
179   - }
180   -
  165 + PageResult<CrmCustomerDO> pageResult = customerService.getPutPoolRemindCustomerPage(pageVO, getLoginUserId());
181 166 // 2. 拼接数据
182 167 return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal()));
183 168 }
184 169  
185   - @GetMapping("/put-in-pool-remind-count")
  170 + @GetMapping("/put-pool-remind-count")
186 171 @Operation(summary = "获得待进入公海客户数量")
187 172 @PreAuthorize("@ss.hasPermission('crm:customer:query')")
188   - public CommonResult<Long> getPutInPoolRemindCustomerCount() {
189   - // 获取公海配置
190   - CrmCustomerPoolConfigDO poolConfigDO = customerPoolConfigService.getCustomerPoolConfig();
191   - if (ObjUtil.isNull(poolConfigDO)
192   - || Boolean.FALSE.equals(poolConfigDO.getEnabled())
193   - || Boolean.FALSE.equals(poolConfigDO.getNotifyEnabled())) {
194   - throw exception(CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED);
195   - }
196   - CrmCustomerPageReqVO pageVO = new CrmCustomerPageReqVO()
197   - .setPool(null)
198   - .setContactStatus(CrmCustomerPageReqVO.CONTACT_TODAY)
199   - .setSceneType(CrmSceneTypeEnum.OWNER.getType());
200   - return success(customerService.getPutInPoolRemindCustomerCount(pageVO, poolConfigDO, getLoginUserId()));
  173 + public CommonResult<Long> getPutPoolRemindCustomerCount() {
  174 + return success(customerService.getPutPoolRemindCustomerCount(getLoginUserId()));
201 175 }
202 176  
203 177 @GetMapping("/today-customer-count")
... ... @@ -225,24 +199,26 @@ public class CrmCustomerController {
225 199 if (poolConfig == null || !poolConfig.getEnabled()) {
226 200 return MapUtil.empty();
227 201 }
  202 + list = CollectionUtils.filterList(list, customer -> {
  203 + // 特殊:如果没负责人,则说明已经在公海,不用计算
  204 + if (customer.getOwnerUserId() == null) {
  205 + return false;
  206 + }
  207 + // 已成交 or 已锁定,不进入公海
  208 + return !customer.getDealStatus() && !customer.getLockStatus();
  209 + });
228 210 return convertMap(list, CrmCustomerDO::getId, customer -> {
229   - // TODO 芋艿:这样计算,貌似有点问题
230 211 // 1.1 未成交放入公海天数
231   - long dealExpireDay = 0;
232   - if (!customer.getDealStatus()) {
233   - dealExpireDay = poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getCreateTime());
234   - }
235   - if (dealExpireDay < 0) {
236   - dealExpireDay = 0;
237   - }
  212 + long dealExpireDay = poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getOwnerTime());
238 213 // 1.2 未跟进放入公海天数
239   - LocalDateTime lastTime = ObjUtil.defaultIfNull(customer.getContactLastTime(), customer.getCreateTime());
240   - long contactExpireDay = poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime);
241   - if (contactExpireDay < 0) {
242   - contactExpireDay = 0;
  214 + LocalDateTime lastTime = customer.getOwnerTime();
  215 + if (customer.getContactLastTime() != null && customer.getContactLastTime().isAfter(lastTime)) {
  216 + lastTime = customer.getContactLastTime();
243 217 }
  218 + long contactExpireDay = poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime);
244 219 // 2. 返回最小的天数
245   - return Math.min(dealExpireDay, contactExpireDay);
  220 + long poolDay = Math.min(dealExpireDay, contactExpireDay);
  221 + return poolDay > 0 ? poolDay : 0;
246 222 });
247 223 }
248 224  
... ...
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 {
57 57 * 关联 AdminUserDO 的 id 字段
58 58 */
59 59 private Long ownerUserId;
  60 + /**
  61 + * 成为负责人的时间
  62 + */
  63 + private LocalDateTime ownerTime;
60 64  
61 65 /**
62 66 * 锁定状态
... ...
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;
31 31 @Mapper
32 32 public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
33 33  
34   - private static MPJLambdaWrapperX<CrmCustomerDO> buildPutInPoolRemindCustomerWrapper(CrmCustomerPageReqVO pageReqVO, CrmCustomerPoolConfigDO poolConfigDO, Long userId) {
35   - MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
36   - // 拼接数据权限的查询条件
37   - CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
38   - CrmCustomerDO::getId, userId, pageReqVO.getSceneType(), null);
39   -
40   - // 锁定状态不需要提醒
41   - query.ne(CrmCustomerDO::getLockStatus, true);
42   -
43   - // 情况一:未成交提醒日期区间
44   - Integer dealExpireDays = poolConfigDO.getDealExpireDays();
45   - LocalDateTime startDealRemindDate = LocalDateTimeUtil.beginOfDay(LocalDateTime.now())
46   - .minusDays(dealExpireDays);
47   - LocalDateTime endDealRemindDate = LocalDateTimeUtil.endOfDay(LocalDateTime.now())
48   - .minusDays(Math.max(dealExpireDays - poolConfigDO.getNotifyDays(), 0));
49   - // 情况二:未跟进提醒日期区间
50   - Integer contactExpireDays = poolConfigDO.getContactExpireDays();
51   - LocalDateTime startContactRemindDate = LocalDateTimeUtil.beginOfDay(LocalDateTime.now())
52   - .minusDays(contactExpireDays);
53   - LocalDateTime endContactRemindDate = LocalDateTimeUtil.endOfDay(LocalDateTime.now())
54   - .minusDays(Math.max(contactExpireDays - poolConfigDO.getNotifyDays(), 0));
55   - query
56   - // 情况一:1. 未成交放入公海提醒
57   - .eq(CrmCustomerDO::getDealStatus, false)
58   - .between(CrmCustomerDO::getCreateTime, startDealRemindDate, endDealRemindDate)
59   - // 情况二:未跟进放入公海提醒
60   - .or() // 2.1 contactLastTime 为空 TODO 芋艿:这个要不要搞个默认值;
61   - .isNull(CrmCustomerDO::getContactLastTime)
62   - .between(CrmCustomerDO::getCreateTime, startContactRemindDate, endContactRemindDate)
63   - .or() // 2.2 ContactLastTime 不为空
64   - .between(CrmCustomerDO::getContactLastTime, startContactRemindDate, endContactRemindDate);
65   - return query;
66   - }
67   -
68 34 default Long selectCountByLockStatusAndOwnerUserId(Boolean lockStatus, Long ownerUserId) {
69 35 return selectCount(new LambdaUpdateWrapper<CrmCustomerDO>()
70 36 .eq(CrmCustomerDO::getLockStatus, lockStatus)
... ... @@ -124,30 +90,80 @@ public interface CrmCustomerMapper extends BaseMapperX&lt;CrmCustomerDO&gt; {
124 90 return selectJoinList(CrmCustomerDO.class, query);
125 91 }
126 92  
127   - default List<CrmCustomerDO> selectListByLockAndNotPool(Boolean lockStatus) {
128   - return selectList(new LambdaQueryWrapper<CrmCustomerDO>()
129   - .eq(CrmCustomerDO::getLockStatus, lockStatus)
130   - .gt(CrmCustomerDO::getOwnerUserId, 0));
131   - }
132   -
133 93 default CrmCustomerDO selectByCustomerName(String name) {
134 94 return selectOne(CrmCustomerDO::getName, name);
135 95 }
136 96  
137   - default PageResult<CrmCustomerDO> selectPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO,
138   - CrmCustomerPoolConfigDO poolConfigDO,
139   - Long userId) {
140   - final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutInPoolRemindCustomerWrapper(pageReqVO, poolConfigDO, userId);
  97 + default PageResult<CrmCustomerDO> selectPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO,
  98 + CrmCustomerPoolConfigDO poolConfig,
  99 + Long userId) {
  100 + final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutPoolRemindCustomerQuery(pageReqVO, poolConfig, userId);
141 101 return selectJoinPage(pageReqVO, CrmCustomerDO.class, query.selectAll(CrmCustomerDO.class));
142 102 }
143 103  
144   - default Long selectPutInPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO,
145   - CrmCustomerPoolConfigDO poolConfigDO,
146   - Long userId) {
147   - final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutInPoolRemindCustomerWrapper(pageReqVO, poolConfigDO, userId);
  104 + default Long selectPutPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO,
  105 + CrmCustomerPoolConfigDO poolConfigDO,
  106 + Long userId) {
  107 + final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutPoolRemindCustomerQuery(pageReqVO, poolConfigDO, userId);
148 108 return selectCount(query);
149 109 }
150 110  
  111 + private static MPJLambdaWrapperX<CrmCustomerDO> buildPutPoolRemindCustomerQuery(CrmCustomerPageReqVO pageReqVO,
  112 + CrmCustomerPoolConfigDO poolConfig,
  113 + Long userId) {
  114 + MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
  115 + // 拼接数据权限的查询条件
  116 + CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
  117 + CrmCustomerDO::getId, userId, pageReqVO.getSceneType(), null);
  118 +
  119 + // 未锁定 + 未成交
  120 + query.eq(CrmCustomerDO::getLockStatus, false).eq(CrmCustomerDO::getDealStatus, false);
  121 +
  122 + // 情况一:未成交提醒日期区间
  123 + Integer dealExpireDays = poolConfig.getDealExpireDays();
  124 + LocalDateTime startDealRemindTime = LocalDateTime.now().minusDays(dealExpireDays);
  125 + LocalDateTime endDealRemindTime = LocalDateTime.now()
  126 + .minusDays(Math.max(dealExpireDays - poolConfig.getNotifyDays(), 0));
  127 + // 情况二:未跟进提醒日期区间
  128 + Integer contactExpireDays = poolConfig.getContactExpireDays();
  129 + LocalDateTime startContactRemindTime = LocalDateTime.now().minusDays(contactExpireDays);
  130 + LocalDateTime endContactRemindTime = LocalDateTime.now()
  131 + .minusDays(Math.max(contactExpireDays - poolConfig.getNotifyDays(), 0));
  132 + query.and(q -> {
  133 + // 情况一:成交超时提醒
  134 + q.between(CrmCustomerDO::getOwnerTime, startDealRemindTime, endDealRemindTime)
  135 + // 情况二:跟进超时提醒
  136 + .or(w -> w.between(CrmCustomerDO::getOwnerTime, startContactRemindTime, endContactRemindTime)
  137 + .and(p -> p.between(CrmCustomerDO::getContactLastTime, startContactRemindTime, endContactRemindTime)
  138 + .or().isNull(CrmCustomerDO::getContactLastTime)));
  139 + });
  140 + return query;
  141 + }
  142 +
  143 + /**
  144 + * 获得需要过期到公海的客户列表
  145 + *
  146 + * @return 客户列表
  147 + */
  148 + default List<CrmCustomerDO> selectListByAutoPool(CrmCustomerPoolConfigDO poolConfig) {
  149 + LambdaQueryWrapper<CrmCustomerDO> query = new LambdaQueryWrapper<>();
  150 + query.gt(CrmCustomerDO::getOwnerUserId, 0);
  151 + // 未锁定 + 未成交
  152 + query.eq(CrmCustomerDO::getLockStatus, false).eq(CrmCustomerDO::getDealStatus, false);
  153 + // 已经超时
  154 + LocalDateTime dealExpireTime = LocalDateTime.now().minusDays(poolConfig.getDealExpireDays());
  155 + LocalDateTime contactExpireTime = LocalDateTime.now().minusDays(poolConfig.getContactExpireDays());
  156 + query.and(q -> {
  157 + // 情况一:成交超时
  158 + q.lt(CrmCustomerDO::getOwnerTime, dealExpireTime)
  159 + // 情况二:跟进超时
  160 + .or(w -> w.lt(CrmCustomerDO::getOwnerTime, contactExpireTime)
  161 + .and(p -> p.lt(CrmCustomerDO::getContactLastTime, contactExpireTime)
  162 + .or().isNull(CrmCustomerDO::getContactLastTime)));
  163 + });
  164 + return selectList(query);
  165 + }
  166 +
151 167 default Long selectTodayCustomerCount(Long userId) {
152 168 MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
153 169 // 我负责的 + 非公海
... ...
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 {
78 78 adminUserApi.validateUser(createReqVO.getOwnerUserId());
79 79  
80 80 // 2. 插入线索
81   - CrmClueDO clue = BeanUtils.toBean(createReqVO, CrmClueDO.class)
82   - .setContactLastTime(LocalDateTime.now());
  81 + CrmClueDO clue = BeanUtils.toBean(createReqVO, CrmClueDO.class);
83 82 clueMapper.insert(clue);
84 83  
85 84 // 3. 创建数据权限
... ... @@ -129,7 +128,7 @@ public class CrmClueServiceImpl implements CrmClueService {
129 128 // 校验线索是否存在
130 129 CrmClueDO oldClue = validateClueExists(id);
131 130  
132   - // 更新
  131 + // 更新线索
133 132 clueMapper.updateById(new CrmClueDO().setId(id).setFollowUpStatus(true).setContactNextTime(contactNextTime)
134 133 .setContactLastTime(LocalDateTime.now()).setContactLastContent(contactLastContent));
135 134  
... ...
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;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
5 5 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
6   -import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
7 6 import cn.iocoder.yudao.module.crm.service.customer.bo.CrmCustomerCreateReqBO;
8   -import cn.iocoder.yudao.module.crm.service.followup.bo.CrmUpdateFollowUpReqBO;
9 7 import jakarta.validation.Valid;
10 8  
  9 +import java.time.LocalDateTime;
11 10 import java.util.Collection;
12 11 import java.util.List;
13 12 import java.util.Map;
... ... @@ -46,6 +45,15 @@ public interface CrmCustomerService {
46 45 void updateCustomerDealStatus(Long id, Boolean dealStatus);
47 46  
48 47 /**
  48 + * 更新客户相关的跟进信息
  49 + *
  50 + * @param id 编号
  51 + * @param contactNextTime 下次联系时间
  52 + * @param contactLastContent 最后联系内容
  53 + */
  54 + void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
  55 +
  56 + /**
49 57 * 删除客户
50 58 *
51 59 * @param id 编号
... ... @@ -89,6 +97,23 @@ public interface CrmCustomerService {
89 97 PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId);
90 98  
91 99 /**
  100 + * 获得放入公海提醒的客户分页
  101 + *
  102 + * @param pageVO 分页查询
  103 + * @param userId 用户编号
  104 + * @return 客户分页
  105 + */
  106 + PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId);
  107 +
  108 + /**
  109 + * 获得待进入公海的客户数量
  110 + *
  111 + * @param userId 用户编号
  112 + * @return 提醒数量
  113 + */
  114 + Long getPutPoolRemindCustomerCount(Long userId);
  115 +
  116 + /**
92 117 * 校验客户是否存在
93 118 *
94 119 * @param customerId 客户 id
... ... @@ -112,13 +137,6 @@ public interface CrmCustomerService {
112 137 void lockCustomer(@Valid CrmCustomerLockReqVO lockReqVO, Long userId);
113 138  
114 139 /**
115   - * 更新客户相关更进信息
116   - *
117   - * @param customerUpdateFollowUpReqBO 请求
118   - */
119   - void updateCustomerFollowUp(CrmUpdateFollowUpReqBO customerUpdateFollowUpReqBO);
120   -
121   - /**
122 140 * 创建客户
123 141 *
124 142 * @param customerCreateReq 请求信息
... ... @@ -162,18 +180,6 @@ public interface CrmCustomerService {
162 180 int autoPutCustomerPool();
163 181  
164 182 /**
165   - * 获得放入公海提醒的客户分页数据
166   - *
167   - * @param pageVO 分页查询
168   - * @param poolConfigDO 公海配置
169   - * @param userId 用户编号
170   - * @return 客户分页
171   - */
172   - PageResult<CrmCustomerDO> getPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO,
173   - CrmCustomerPoolConfigDO poolConfigDO,
174   - Long userId);
175   -
176   - /**
177 183 * 获得今日需联系客户数量
178 184 *
179 185 * @param userId 用户编号
... ... @@ -182,18 +188,6 @@ public interface CrmCustomerService {
182 188 Long getTodayCustomerCount(Long userId);
183 189  
184 190 /**
185   - * 获得待进入公海的客户数量
186   - *
187   - * @param pageVO 分页查询
188   - * @param poolConfigDO 公海配置
189   - * @param userId 用户编号
190   - * @return 提醒数量
191   - */
192   - Long getPutInPoolRemindCustomerCount(CrmCustomerPageReqVO pageVO,
193   - CrmCustomerPoolConfigDO poolConfigDO,
194   - Long userId);
195   -
196   - /**
197 191 * 获得分配给我的客户数量
198 192 *
199 193 * @param userId 用户编号
... ...
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;
8 8 import cn.iocoder.yudao.framework.common.exception.ServiceException;
9 9 import cn.iocoder.yudao.framework.common.pojo.PageResult;
10 10 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
11   -import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
12 11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
13 12 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
14 13 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
... ... @@ -16,6 +15,7 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfi
16 15 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
17 16 import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
18 17 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
  18 +import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
19 19 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
20 20 import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
21 21 import cn.iocoder.yudao.module.crm.framework.permission.core.util.CrmPermissionUtils;
... ... @@ -23,7 +23,6 @@ import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
23 23 import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
24 24 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
25 25 import cn.iocoder.yudao.module.crm.service.customer.bo.CrmCustomerCreateReqBO;
26   -import cn.iocoder.yudao.module.crm.service.followup.bo.CrmUpdateFollowUpReqBO;
27 26 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
28 27 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
29 28 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
... ... @@ -43,7 +42,6 @@ import java.time.LocalDateTime;
43 42 import java.util.*;
44 43  
45 44 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
46   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
47 45 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
48 46 import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
49 47 import static cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLimitConfigTypeEnum.CUSTOMER_LOCK_LIMIT;
... ... @@ -114,7 +112,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
114 112 */
115 113 private static CrmCustomerDO initCustomer(Object customer, Long ownerUserId) {
116 114 return BeanUtils.toBean(customer, CrmCustomerDO.class).setOwnerUserId(ownerUserId)
117   - .setLockStatus(false).setDealStatus(false).setContactLastTime(LocalDateTime.now());
  115 + .setOwnerTime(LocalDateTime.now());
118 116 }
119 117  
120 118 @Override
... ... @@ -158,6 +156,22 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
158 156 }
159 157  
160 158 @Override
  159 + @LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}",
  160 + success = CRM_CUSTOMER_FOLLOW_UP_SUCCESS)
  161 + @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
  162 + public void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) {
  163 + // 1.1 校验存在
  164 + CrmCustomerDO customer = validateCustomerExists(id);
  165 +
  166 + // 2. 更新客户的跟进信息
  167 + customerMapper.updateById(new CrmCustomerDO().setId(id).setFollowUpStatus(true).setContactNextTime(contactNextTime)
  168 + .setContactLastTime(LocalDateTime.now()));
  169 +
  170 + // 3. 记录操作日志上下文
  171 + LogRecordContext.putVariable("customerName", customer.getName());
  172 + }
  173 +
  174 + @Override
161 175 @Transactional(rollbackFor = Exception.class)
162 176 @LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_DELETE_SUB_TYPE, bizNo = "{{#id}}",
163 177 success = CRM_CUSTOMER_DELETE_SUCCESS)
... ... @@ -168,7 +182,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
168 182 // 1.2 检查引用
169 183 validateCustomerReference(id);
170 184  
171   - // 2. 删除
  185 + // 2. 删除客户
172 186 customerMapper.deleteById(id);
173 187 // 3. 删除数据权限
174 188 permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), id);
... ... @@ -209,7 +223,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
209 223 permissionService.transferPermission(new CrmPermissionTransferReqBO(userId, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
210 224 reqVO.getId(), reqVO.getNewOwnerUserId(), reqVO.getOldOwnerPermissionLevel()));
211 225 // 2.2 转移后重新设置负责人
212   - customerMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
  226 + customerMapper.updateById(new CrmCustomerDO().setId(reqVO.getId())
  227 + .setOwnerUserId(reqVO.getNewOwnerUserId()).setOwnerTime(LocalDateTime.now()));
213 228  
214 229 // 3. 记录转移日志
215 230 LogRecordContext.putVariable("customer", customer);
... ... @@ -226,7 +241,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
226 241 if (customer.getLockStatus().equals(lockReqVO.getLockStatus())) {
227 242 throw exception(customer.getLockStatus() ? CUSTOMER_LOCK_FAIL_IS_LOCK : CUSTOMER_UNLOCK_FAIL_IS_UNLOCK);
228 243 }
229   - // 1.3 校验锁定上限。
  244 + // 1.3 校验锁定上限
230 245 if (lockReqVO.getLockStatus()) {
231 246 validateCustomerExceedLockLimit(userId);
232 247 }
... ... @@ -240,11 +255,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
240 255 }
241 256  
242 257 @Override
243   - public void updateCustomerFollowUp(CrmUpdateFollowUpReqBO customerUpdateFollowUpReqBO) {
244   - customerMapper.updateById(BeanUtils.toBean(customerUpdateFollowUpReqBO, CrmCustomerDO.class).setId(customerUpdateFollowUpReqBO.getBizId()));
245   - }
246   -
247   - @Override
248 258 @Transactional(rollbackFor = Exception.class)
249 259 @LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_CREATE_SUB_TYPE, bizNo = "{{#customer.id}}",
250 260 success = CRM_CUSTOMER_CREATE_SUCCESS)
... ... @@ -263,7 +273,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
263 273 }
264 274  
265 275 @Override
266   - public CrmCustomerImportRespVO importCustomerList(List<CrmCustomerImportExcelVO> importCustomers, CrmCustomerImportReqVO importReqVO) {
  276 + public CrmCustomerImportRespVO importCustomerList(List<CrmCustomerImportExcelVO> importCustomers,
  277 + CrmCustomerImportReqVO importReqVO) {
267 278 if (CollUtil.isEmpty(importCustomers)) {
268 279 throw exception(CUSTOMER_IMPORT_LIST_IS_EMPTY);
269 280 }
... ... @@ -383,12 +394,13 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
383 394 // 1.4 校验负责人是否到达上限
384 395 validateCustomerExceedOwnerLimit(ownerUserId, customers.size());
385 396  
386   - // 2.1 领取公海数据
  397 + // 2. 领取公海数据
387 398 List<CrmCustomerDO> updateCustomers = new ArrayList<>();
388 399 List<CrmPermissionCreateReqBO> createPermissions = new ArrayList<>();
389 400 customers.forEach(customer -> {
390 401 // 2.1. 设置负责人
391   - updateCustomers.add(new CrmCustomerDO().setId(customer.getId()).setOwnerUserId(ownerUserId));
  402 + updateCustomers.add(new CrmCustomerDO().setId(customer.getId())
  403 + .setOwnerUserId(ownerUserId).setOwnerTime(LocalDateTime.now()));
392 404 // 2.2. 创建负责人数据权限
393 405 createPermissions.add(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
394 406 .setBizId(customer.getId()).setUserId(ownerUserId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
... ... @@ -415,34 +427,23 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
415 427 if (poolConfig == null || !poolConfig.getEnabled()) {
416 428 return 0;
417 429 }
418   - // 1.1 获取没有锁定的不在公海的客户
419   - List<CrmCustomerDO> customerList = customerMapper.selectListByLockAndNotPool(Boolean.FALSE);
420   - // TODO @puhui999:下面也搞到 sql 里去哈;写 or 查询,问题不大的;低 393 到 402;原因是,避免无用的太多数据查询到 java 进程里;
421   - List<CrmCustomerDO> poolCustomerList = new ArrayList<>();
422   - poolCustomerList.addAll(filterList(customerList, customer ->
423   - !customer.getDealStatus() && (poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getCreateTime())) <= 0));
424   - poolCustomerList.addAll(filterList(customerList, customer -> {
425   - if (!customer.getDealStatus()) { // 这里只处理成交的
426   - return false;
427   - }
428   - LocalDateTime lastTime = ObjUtil.defaultIfNull(customer.getContactLastTime(), customer.getCreateTime());
429   - return (poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime)) <= 0;
430   - }));
431   -
  430 + // 1. 获得需要放到的客户列表
  431 + List<CrmCustomerDO> customerList = customerMapper.selectListByAutoPool(poolConfig);
432 432 // 2. 逐个放入公海
433 433 int count = 0;
434   - for (CrmCustomerDO customer : poolCustomerList) {
  434 + for (CrmCustomerDO customer : customerList) {
435 435 try {
436 436 getSelf().putCustomerPool(customer);
437 437 count++;
438 438 } catch (Throwable e) {
439   - log.error("[autoPutCustomerPool][Customer 客户({}) 放入公海异常]", customer.getId(), e);
  439 + log.error("[autoPutCustomerPool][客户({}) 放入公海异常]", customer.getId(), e);
440 440 }
441 441 }
442 442 return count;
443 443 }
444 444  
445   - private void putCustomerPool(CrmCustomerDO customer) {
  445 + @Transactional // 需要 protected 修饰,因为需要在事务中调用
  446 + protected void putCustomerPool(CrmCustomerDO customer) {
446 447 // 1. 设置负责人为 NULL
447 448 int updateOwnerUserIncr = customerMapper.updateOwnerUserIdById(customer.getId(), null);
448 449 if (updateOwnerUserIncr == 0) {
... ... @@ -486,17 +487,29 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
486 487 }
487 488  
488 489 @Override
489   - public PageResult<CrmCustomerDO> getPutInPoolRemindCustomerPage(CrmCustomerPageReqVO pageReqVO,
490   - CrmCustomerPoolConfigDO poolConfigDO,
491   - Long userId) {
492   - return customerMapper.selectPutInPoolRemindCustomerPage(pageReqVO, poolConfigDO, userId);
  490 + public PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId) {
  491 + CrmCustomerPoolConfigDO poolConfig = customerPoolConfigService.getCustomerPoolConfig();
  492 + if (ObjUtil.isNull(poolConfig)
  493 + || Boolean.FALSE.equals(poolConfig.getEnabled())
  494 + || Boolean.FALSE.equals(poolConfig.getNotifyEnabled())) {
  495 + return PageResult.empty();
  496 + }
  497 + return customerMapper.selectPutPoolRemindCustomerPage(pageVO, poolConfig, userId);
493 498 }
494 499  
495 500 @Override
496   - public Long getPutInPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO,
497   - CrmCustomerPoolConfigDO poolConfigDO,
498   - Long userId) {
499   - return customerMapper.selectPutInPoolRemindCustomerCount(pageReqVO, poolConfigDO, userId);
  501 + public Long getPutPoolRemindCustomerCount(Long userId) {
  502 + CrmCustomerPoolConfigDO poolConfig = customerPoolConfigService.getCustomerPoolConfig();
  503 + if (ObjUtil.isNull(poolConfig)
  504 + || Boolean.FALSE.equals(poolConfig.getEnabled())
  505 + || Boolean.FALSE.equals(poolConfig.getNotifyEnabled())) {
  506 + return 0L;
  507 + }
  508 + CrmCustomerPageReqVO pageVO = new CrmCustomerPageReqVO()
  509 + .setPool(null)
  510 + .setContactStatus(CrmCustomerPageReqVO.CONTACT_TODAY)
  511 + .setSceneType(CrmSceneTypeEnum.OWNER.getType());
  512 + return customerMapper.selectPutPoolRemindCustomerCount(pageVO, poolConfig, userId);
500 513 }
501 514  
502 515 @Override
... ... @@ -596,7 +609,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
596 609 }
597 610 }
598 611  
599   -
600 612 /**
601 613 * 获得自身的代理对象,解决 AOP 生效问题
602 614 *
... ...
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 {
72 72 CrmFollowUpRecordDO followUpRecord = BeanUtils.toBean(createReqVO, CrmFollowUpRecordDO.class);
73 73 crmFollowUpRecordMapper.insert(followUpRecord);
74 74  
75   - LocalDateTime now = LocalDateTime.now();
76   - CrmUpdateFollowUpReqBO updateFollowUpReqBO = new CrmUpdateFollowUpReqBO().setBizId(followUpRecord.getBizId())
77   - .setContactLastTime(now).setContactNextTime(followUpRecord.getNextTime()).setContactLastContent(followUpRecord.getContent());
78 75 // 2. 更新 bizId 对应的记录;
79   - if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_BUSINESS.getType(), followUpRecord.getBizType())) { // 更新商机跟进信息
  76 + CrmUpdateFollowUpReqBO updateFollowUpReqBO = new CrmUpdateFollowUpReqBO().setBizId(followUpRecord.getBizId())
  77 + .setContactLastTime(LocalDateTime.now())
  78 + .setContactNextTime(followUpRecord.getNextTime()).setContactLastContent(followUpRecord.getContent());
  79 + if (ObjUtil.equal(CrmBizTypeEnum.CRM_BUSINESS.getType(), followUpRecord.getBizType())) { // 更新商机跟进信息
80 80 businessService.updateBusinessFollowUpBatch(Collections.singletonList(updateFollowUpReqBO));
81 81 }
82   - if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CLUE.getType(), followUpRecord.getBizType())) { // 更新线索跟进信息
  82 + if (ObjUtil.equal(CrmBizTypeEnum.CRM_CLUE.getType(), followUpRecord.getBizType())) { // 更新线索跟进信息
83 83 clueService.updateClueFollowUp(followUpRecord.getId(), followUpRecord.getNextTime(), followUpRecord.getContent());
84 84 }
85   - if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CONTACT.getType(), followUpRecord.getBizType())) { // 更新联系人跟进信息
  85 + if (ObjUtil.equal(CrmBizTypeEnum.CRM_CONTACT.getType(), followUpRecord.getBizType())) { // 更新联系人跟进信息
86 86 contactService.updateContactFollowUpBatch(Collections.singletonList(updateFollowUpReqBO));
87 87 }
88   - if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CONTRACT.getType(), followUpRecord.getBizType())) { // 更新合同跟进信息
  88 + if (ObjUtil.equal(CrmBizTypeEnum.CRM_CONTRACT.getType(), followUpRecord.getBizType())) { // 更新合同跟进信息
89 89 contractService.updateContractFollowUp(updateFollowUpReqBO);
90 90 }
91   - if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CUSTOMER.getType(), followUpRecord.getBizType())) { // 更新客户跟进信息
92   - customerService.updateCustomerFollowUp(updateFollowUpReqBO);
  91 + if (ObjUtil.equal(CrmBizTypeEnum.CRM_CUSTOMER.getType(), followUpRecord.getBizType())) { // 更新客户跟进信息
  92 + customerService.updateCustomerFollowUp(followUpRecord.getBizId(), followUpRecord.getNextTime(), followUpRecord.getContent());
93 93 }
94 94  
95 95 // 3.1 更新 contactIds 对应的记录,不更新 lastTime 和 lastContent
... ...