Browse Source

📖 CRM:线索的跟进逻辑

YunaiV 1 year ago
parent
commit
69a974ef02

+ 1 - 2
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java

@@ -16,8 +16,7 @@ public interface ErrorCodeConstants {
 
     // ========== 线索管理 1-020-001-000 ==========
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
-    ErrorCode CLUE_NOT_EXISTS_ANY = new ErrorCode(1_020_001_001, "线索【{}】不存在");
-    ErrorCode CLUE_TRANSFORM_FAIL_ALREADY = new ErrorCode(1_020_001_002, "线索【{}】已经转化过了,请勿重复转化");
+    ErrorCode CLUE_TRANSFORM_FAIL_ALREADY = new ErrorCode(1_020_001_001, "线索已经转化过了,请勿重复转化");
 
     // ========== 商机管理 1-020-002-000 ==========
     ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");

+ 15 - 13
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java

@@ -8,19 +8,21 @@ package cn.iocoder.yudao.module.crm.enums;
  */
 public interface LogRecordConstants {
 
-    // ======================= CRM_LEADS 线索 =======================
-
-    String CRM_LEADS_TYPE = "CRM 线索";
-    String CRM_LEADS_CREATE_SUB_TYPE = "创建线索";
-    String CRM_LEADS_CREATE_SUCCESS = "创建了线索{{#clue.name}}";
-    String CRM_LEADS_UPDATE_SUB_TYPE = "更新线索";
-    String CRM_LEADS_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReq}}";
-    String CRM_LEADS_DELETE_SUB_TYPE = "删除线索";
-    String CRM_LEADS_DELETE_SUCCESS = "删除了线索【{{#clueName}}】";
-    String CRM_LEADS_TRANSFER_SUB_TYPE = "转移线索";
-    String CRM_LEADS_TRANSFER_SUCCESS = "将线索【{{#clue.name}}】的负责人从【{getAdminUserById{#clue.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
-    String CRM_LEADS_TRANSLATE_SUB_TYPE = "线索转化为客户";
-    String CRM_LEADS_TRANSLATE_SUCCESS = "将线索【{{#clue.name}}】转化为客户";
+    // ======================= CRM_CLUE 线索 =======================
+
+    String CRM_CLUE_TYPE = "CRM 线索";
+    String CRM_CLUE_CREATE_SUB_TYPE = "创建线索";
+    String CRM_CLUE_CREATE_SUCCESS = "创建了线索{{#clue.name}}";
+    String CRM_CLUE_UPDATE_SUB_TYPE = "更新线索";
+    String CRM_CLUE_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReq}}";
+    String CRM_CLUE_DELETE_SUB_TYPE = "删除线索";
+    String CRM_CLUE_DELETE_SUCCESS = "删除了线索【{{#clueName}}】";
+    String CRM_CLUE_TRANSFER_SUB_TYPE = "转移线索";
+    String CRM_CLUE_TRANSFER_SUCCESS = "将线索【{{#clue.name}}】的负责人从【{getAdminUserById{#clue.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
+    String CRM_CLUE_TRANSLATE_SUB_TYPE = "线索转化为客户";
+    String CRM_CLUE_TRANSLATE_SUCCESS = "将线索【{{#clueName}}】转化为客户";
+    String CRM_CLUE_FOLLOW_UP_SUB_TYPE = "线索跟进";
+    String CRM_CLUE_FOLLOW_UP_SUCCESS = "线索跟进【{{#clueName}}】";
 
     // ======================= CRM_CUSTOMER 客户 =======================
 

+ 1 - 1
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java

@@ -17,7 +17,7 @@ import java.util.Arrays;
 @Getter
 public enum CrmBizTypeEnum implements IntArrayValuable {
 
-    CRM_LEADS(1, "线索"),
+    CRM_CLUE(1, "线索"),
     CRM_CUSTOMER(2, "客户"),
     CRM_CONTACT(3, "联系人"),
     CRM_BUSINESS(4, "商机"),

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

@@ -9,7 +9,10 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransferReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.service.clue.CrmClueService;
@@ -36,7 +39,8 @@ import java.util.stream.Stream;
 
 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.common.util.collection.CollectionUtils.convertListByFlatMap;
+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;
 
@@ -151,18 +155,18 @@ public class CrmClueController {
 
     @PutMapping("/transform")
     @Operation(summary = "线索转化为客户")
-    @Parameter(name = "ids", description = "线索编号数组", required = true)
+    @Parameter(name = "id", description = "编号", required = true)
     @PreAuthorize("@ss.hasPermission('crm:clue:update')")
-    public CommonResult<Boolean> transformClue(@RequestParam("ids") List<Long> ids) {
-        clueService.transformClue(ids, getLoginUserId());
+    public CommonResult<Boolean> transformClue(@RequestParam("id") Long id) {
+        clueService.transformClue(id, getLoginUserId());
         return success(Boolean.TRUE);
     }
 
-    @GetMapping("/follow-leads-count")
-    @Operation(summary = "获得分配给我的线索数量")
+    @GetMapping("/follow-count")
+    @Operation(summary = "获得分配给我的、待跟进的线索数量")
     @PreAuthorize("@ss.hasPermission('crm:clue:query')")
-    public CommonResult<Long> getFollowLeadsCount() {
-        return success(clueService.getFollowLeadsCount(getLoginUserId()));
+    public CommonResult<Long> getFollowClueCount() {
+        return success(clueService.getFollowClueCount(getLoginUserId()));
     }
 
 }

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/CrmOperateLogController.java

@@ -41,7 +41,7 @@ public class CrmOperateLogController {
     private static final Map<Integer, String> BIZ_TYPE_MAP = new HashMap<>();
 
     static {
-        BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_LEADS.getType(), CRM_LEADS_TYPE);
+        BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_CLUE.getType(), CRM_CLUE_TYPE);
         BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_CUSTOMER.getType(), CRM_CUSTOMER_TYPE);
         BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_CONTACT.getType(), CRM_CONTACT_TYPE);
         BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_BUSINESS.getType(), CRM_BUSINESS_TYPE);

+ 0 - 22
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java

@@ -1,22 +0,0 @@
-package cn.iocoder.yudao.module.crm.convert.clue;
-
-import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransferReqVO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
-import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-import org.mapstruct.factory.Mappers;
-
-/**
- * 线索 Convert
- *
- * @author Wanwan
- */
-@Mapper
-public interface CrmClueConvert {
-
-    CrmClueConvert INSTANCE = Mappers.getMapper(CrmClueConvert.class);
-
-    @Mapping(target = "bizId", source = "reqVO.id")
-    CrmPermissionTransferReqBO convert(CrmClueTransferReqVO reqVO, Long userId);
-
-}

+ 4 - 11
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java

@@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
 import cn.iocoder.yudao.module.crm.util.CrmQueryWrapperUtils;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.Collection;
@@ -22,16 +21,10 @@ import java.util.List;
 @Mapper
 public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
 
-    default int updateOwnerUserIdById(Long id, Long ownerUserId) {
-        return update(new LambdaUpdateWrapper<CrmClueDO>()
-                .eq(CrmClueDO::getId, id)
-                .set(CrmClueDO::getOwnerUserId, ownerUserId));
-    }
-
     default PageResult<CrmClueDO> selectPage(CrmCluePageReqVO pageReqVO, Long userId) {
         MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_LEADS.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CLUE.getType(),
                 CrmClueDO::getId, userId, pageReqVO.getSceneType(), pageReqVO.getPool());
         // 拼接自身的查询条件
         query.selectAll(CrmClueDO.class)
@@ -50,16 +43,16 @@ public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
     default List<CrmClueDO> selectBatchIds(Collection<Long> ids, Long userId) {
         MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_LEADS.getType(), ids, userId);
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CLUE.getType(), ids, userId);
         query.selectAll(CrmClueDO.class).in(CrmClueDO::getId, ids).orderByDesc(CrmClueDO::getId);
         // 拼接自身的查询条件
         return selectJoinList(CrmClueDO.class, query);
     }
 
-    default Long selectFollowLeadsCount(Long userId) {
+    default Long selectCountByFollow(Long userId) {
         MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>();
         // 我负责的 + 非公海
-        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_LEADS.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CLUE.getType(),
                 CrmClueDO::getId, userId, CrmSceneTypeEnum.OWNER.getType(), Boolean.FALSE);
         // 未跟进 + 未转化
         query.eq(CrmClueDO::getFollowUpStatus, false)

+ 10 - 8
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java

@@ -5,9 +5,9 @@ import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueSaveReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransferReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
-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;
 
@@ -36,9 +36,11 @@ public interface CrmClueService {
     /**
      * 更新线索相关的跟进信息
      *
-     * @param clueUpdateFollowUpReqBO 信息
+     * @param id 编号
+     * @param contactNextTime 下次联系时间
+     * @param contactLastContent 最后联系内容
      */
-    void updateClueFollowUp(CrmUpdateFollowUpReqBO clueUpdateFollowUpReqBO);
+    void updateClueFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
 
     /**
      * 删除线索
@@ -83,17 +85,17 @@ public interface CrmClueService {
     /**
      * 线索转化为客户
      *
-     * @param ids  线索编号数组
+     * @param id  线索编号
      * @param userId 用户编号
      */
-    void transformClue(List<Long> ids, Long userId);
+    void transformClue(Long id, Long userId);
 
     /**
-     * 获得分配给我的线索数量
+     * 获得分配给我的、待跟进的线索数量
      *
      * @param userId 用户编号
-     * @return 提醒数量
+     * @return 数量
      */
-    Long getFollowLeadsCount(Long userId);
+    Long getFollowClueCount(Long userId);
 
 }

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

@@ -3,15 +3,12 @@ package cn.iocoder.yudao.module.crm.service.clue;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueSaveReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransferReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
@@ -25,6 +22,7 @@ import cn.iocoder.yudao.module.crm.service.followup.bo.CrmFollowUpCreateReqBO;
 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;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.service.impl.DiffParseFunction;
@@ -37,12 +35,13 @@ import org.springframework.validation.annotation.Validated;
 import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CLUE_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CLUE_TRANSFORM_FAIL_ALREADY;
 import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_NOT_EXISTS;
 
@@ -60,18 +59,18 @@ public class CrmClueServiceImpl implements CrmClueService {
 
     @Resource
     private CrmCustomerService customerService;
-
     @Resource
     private CrmPermissionService crmPermissionService;
     @Resource
     private CrmFollowUpRecordService followUpRecordService;
+
     @Resource
     private AdminUserApi adminUserApi;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @LogRecord(type = CRM_LEADS_TYPE, subType = CRM_LEADS_CREATE_SUB_TYPE, bizNo = "{{#clue.id}}",
-            success = CRM_LEADS_CREATE_SUCCESS)
+    @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_CREATE_SUB_TYPE, bizNo = "{{#clue.id}}",
+            success = CRM_CLUE_CREATE_SUCCESS)
     public Long createClue(CrmClueSaveReqVO createReqVO) {
         // 1.1 校验关联数据
         validateRelationDataExists(createReqVO);
@@ -84,7 +83,7 @@ public class CrmClueServiceImpl implements CrmClueService {
         clueMapper.insert(clue);
 
         // 3. 创建数据权限
-        CrmPermissionCreateReqBO createReqBO = new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_LEADS.getType())
+        CrmPermissionCreateReqBO createReqBO = new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CLUE.getType())
                 .setBizId(clue.getId()).setUserId(clue.getOwnerUserId()).setLevel(CrmPermissionLevelEnum.OWNER.getLevel());
         crmPermissionService.createPermission(createReqBO);
 
@@ -95,9 +94,9 @@ public class CrmClueServiceImpl implements CrmClueService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @LogRecord(type = CRM_LEADS_TYPE, subType = CRM_LEADS_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
-            success = CRM_LEADS_UPDATE_SUCCESS)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_LEADS, bizId = "#updateReq.id", level = CrmPermissionLevelEnum.OWNER)
+    @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
+            success = CRM_CLUE_UPDATE_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReq.id", level = CrmPermissionLevelEnum.OWNER)
     public void updateClue(CrmClueSaveReqVO updateReq) {
         Assert.notNull(updateReq.getId(), "线索编号不能为空");
         // 1.1 校验线索是否存在
@@ -114,15 +113,25 @@ public class CrmClueServiceImpl implements CrmClueService {
         LogRecordContext.putVariable("clueName", oldClue.getName());
     }
 
+    private void validateRelationDataExists(CrmClueSaveReqVO reqVO) {
+        // 校验负责人
+        if (Objects.nonNull(reqVO.getOwnerUserId()) &&
+                Objects.isNull(adminUserApi.getUser(reqVO.getOwnerUserId()))) {
+            throw exception(USER_NOT_EXISTS);
+        }
+    }
+
     @Override
-    @LogRecord(type = CRM_LEADS_TYPE, subType = CRM_LEADS_UPDATE_SUB_TYPE, bizNo = "{{#updateReq.bizId}",
-            success = CRM_LEADS_UPDATE_SUCCESS)
-    public void updateClueFollowUp(CrmUpdateFollowUpReqBO updateReq) {
+    @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}",
+            success = CRM_CLUE_FOLLOW_UP_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
+    public void updateClueFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) {
         // 校验线索是否存在
-        CrmClueDO oldClue = validateClueExists(updateReq.getBizId());
+        CrmClueDO oldClue = validateClueExists(id);
 
         // 更新
-        clueMapper.updateById(BeanUtils.toBean(updateReq, CrmClueDO.class).setId(updateReq.getBizId()));
+        clueMapper.updateById(new CrmClueDO().setId(id).setFollowUpStatus(true).setContactNextTime(contactNextTime)
+                .setContactLastTime(LocalDateTime.now()).setContactLastContent(contactLastContent));
 
         // 3. 记录操作日志上下文
         LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmUpdateFollowUpReqBO.class));
@@ -131,9 +140,9 @@ public class CrmClueServiceImpl implements CrmClueService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @LogRecord(type = CRM_LEADS_TYPE, subType = CRM_LEADS_DELETE_SUB_TYPE, bizNo = "{{#id}}",
-            success = CRM_LEADS_DELETE_SUCCESS)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_LEADS, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
+    @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_DELETE_SUB_TYPE, bizNo = "{{#id}}",
+            success = CRM_CLUE_DELETE_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteClue(Long id) {
         // 1. 校验存在
         CrmClueDO clue = validateClueExists(id);
@@ -142,28 +151,29 @@ public class CrmClueServiceImpl implements CrmClueService {
         clueMapper.deleteById(id);
 
         // 3. 删除数据权限
-        crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_LEADS.getType(), id);
+        crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_CLUE.getType(), id);
 
         // 4. 删除跟进
-        followUpRecordService.deleteFollowUpRecordByBiz(CrmBizTypeEnum.CRM_LEADS.getType(), id);
+        followUpRecordService.deleteFollowUpRecordByBiz(CrmBizTypeEnum.CRM_CLUE.getType(), id);
 
-        // 记录操作日志上下文
+        // 5. 记录操作日志上下文
         LogRecordContext.putVariable("clueName", clue.getName());
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @LogRecord(type = CRM_LEADS_TYPE, subType = CRM_LEADS_TRANSFER_SUB_TYPE, bizNo = "{{#reqVO.id}}",
-            success = CRM_LEADS_TRANSFER_SUCCESS)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_LEADS, bizId = "#reqVO.id", level = CrmPermissionLevelEnum.OWNER)
+    @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_TRANSFER_SUB_TYPE, bizNo = "{{#reqVO.id}}",
+            success = CRM_CLUE_TRANSFER_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#reqVO.id", level = CrmPermissionLevelEnum.OWNER)
     public void transferClue(CrmClueTransferReqVO reqVO, Long userId) {
         // 1 校验线索是否存在
         CrmClueDO clue = validateClueExists(reqVO.getId());
 
         // 2.1 数据权限转移
-        crmPermissionService.transferPermission(CrmClueConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_LEADS.getType()));
+        crmPermissionService.transferPermission(new CrmPermissionTransferReqBO(userId, CrmBizTypeEnum.CRM_CLUE.getType(),
+                        reqVO.getId(), reqVO.getNewOwnerUserId(), reqVO.getOldOwnerPermissionLevel()));
         // 2.2 设置新的负责人
-        clueMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
+        clueMapper.updateById(new CrmClueDO().setId(reqVO.getId()).setOwnerUserId(reqVO.getNewOwnerUserId()));
 
         // 3. 记录转移日志
         LogRecordContext.putVariable("clue", clue);
@@ -171,69 +181,32 @@ public class CrmClueServiceImpl implements CrmClueService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_LEADS, bizId = "#ids", level = CrmPermissionLevelEnum.OWNER)
-    public void transformClue(List<Long> ids, Long userId) {
+    @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_TRANSLATE_SUB_TYPE, bizNo = "{{#id}}",
+            success = CRM_CLUE_TRANSLATE_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
+    public void transformClue(Long id, Long userId) {
         // 1.1 校验线索都存在
-        List<CrmClueDO> clues = getClueList(ids, userId);
-        if (CollUtil.isEmpty(clues) || ObjectUtil.notEqual(clues.size(), ids.size())) {
-            ids.removeAll(convertSet(clues, CrmClueDO::getId));
-            throw exception(CLUE_NOT_EXISTS_ANY, ids);
-        }
-        // 1.2 存在已经转化的,直接提示哈。避免操作的用户,以为都转化成功了
-        List<CrmClueDO> translatedClues = filterList(clues,
-                clue -> ObjectUtil.equal(Boolean.TRUE, clue.getTransformStatus()));
-        if (CollUtil.isNotEmpty(translatedClues)) {
-            throw exception(CLUE_TRANSFORM_FAIL_ALREADY, convertSet(translatedClues, CrmClueDO::getId));
+        CrmClueDO clue = validateClueExists(id);
+        // 1.2 存在已经转化的
+        if (clue.getTransformStatus()) {
+            throw exception(CLUE_TRANSFORM_FAIL_ALREADY);
         }
 
         // 2.1 遍历线索(未转化的线索),创建对应的客户
-        clues.forEach(clue -> {
-            Long customerId = customerService.createCustomer(BeanUtils.toBean(clue, CrmCustomerCreateReqBO.class), userId);
-            clue.setCustomerId(customerId);
-        });
+        Long customerId = customerService.createCustomer(BeanUtils.toBean(clue, CrmCustomerCreateReqBO.class), userId);
         // 2.2 更新线索
-        clueMapper.updateBatch(convertList(clues, clue -> new CrmClueDO().setId(clue.getId())
-                .setTransformStatus(Boolean.TRUE).setCustomerId(clue.getCustomerId())));
+        clueMapper.updateById(new CrmClueDO().setId(id).setTransformStatus(Boolean.TRUE).setCustomerId(customerId));
         // 2.3 复制跟进记录
-        copyFollowUpRecords(clues);
-
-        // 3. 记录操作日志
-        for (CrmClueDO clue : clues) {
-            getSelf().translateCustomerLog(clue);
-        }
-    }
-
-    /**
-     * 线索被转换客户后,需要将线索的跟进记录,复制到客户上
-     *
-     * @param clues 被转化的线索
-     */
-    private void copyFollowUpRecords(List<CrmClueDO> clues) {
         List<CrmFollowUpRecordDO> followUpRecords = followUpRecordService.getFollowUpRecordByBiz(
-                CrmBizTypeEnum.CRM_LEADS.getType(), convertSet(clues, CrmClueDO::getId));
-        if (CollUtil.isEmpty(followUpRecords)) {
-            return;
+                CrmBizTypeEnum.CRM_CLUE.getType(), singleton(clue.getId()));
+        if (CollUtil.isNotEmpty(followUpRecords)) {
+            followUpRecordService.createFollowUpRecordBatch(convertList(followUpRecords, record ->
+                    BeanUtils.toBean(record, CrmFollowUpCreateReqBO.class)
+                            .setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType()).setBizId(customerId)));
         }
-        // 创建跟进
-        Map<Long, CrmClueDO> clueMap = convertMap(clues, CrmClueDO::getId);
-        followUpRecordService.createFollowUpRecordBatch(convertList(followUpRecords, followUpRecord ->
-                BeanUtils.toBean(followUpRecord, CrmFollowUpCreateReqBO.class).setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
-                        .setBizId(clueMap.get(followUpRecord.getBizId()).getCustomerId())));
-    }
-
-    @LogRecord(type = CRM_LEADS_TYPE, subType = CRM_LEADS_TRANSLATE_SUB_TYPE, bizNo = "{{#clue.id}}",
-            success = CRM_LEADS_TRANSLATE_SUCCESS)
-    public void translateCustomerLog(CrmClueDO clue) {
-        // 记录操作日志上下文
-        LogRecordContext.putVariable("clue", clue);
-    }
 
-    private void validateRelationDataExists(CrmClueSaveReqVO reqVO) {
-        // 校验负责人
-        if (Objects.nonNull(reqVO.getOwnerUserId()) &&
-                Objects.isNull(adminUserApi.getUser(reqVO.getOwnerUserId()))) {
-            throw exception(USER_NOT_EXISTS);
-        }
+        // 3. 记录操作日志上下文
+        LogRecordContext.putVariable("clueName", clue.getName());
     }
 
     private CrmClueDO validateClueExists(Long id) {
@@ -245,7 +218,7 @@ public class CrmClueServiceImpl implements CrmClueService {
     }
 
     @Override
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_LEADS, bizId = "#id", level = CrmPermissionLevelEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.READ)
     public CrmClueDO getClue(Long id) {
         return clueMapper.selectById(id);
     }
@@ -263,18 +236,9 @@ public class CrmClueServiceImpl implements CrmClueService {
         return clueMapper.selectPage(pageReqVO, userId);
     }
 
-    /**
-     * 获得自身的代理对象,解决 AOP 生效问题
-     *
-     * @return 自己
-     */
-    private CrmClueServiceImpl getSelf() {
-        return SpringUtil.getBean(getClass());
-    }
-
     @Override
-    public Long getFollowLeadsCount(Long userId) {
-        return clueMapper.selectFollowLeadsCount(userId);
+    public Long getFollowClueCount(Long userId) {
+        return clueMapper.selectCountByFollow(userId);
     }
 
 }

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

@@ -79,8 +79,8 @@ public class CrmFollowUpRecordServiceImpl implements CrmFollowUpRecordService {
         if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_BUSINESS.getType(), followUpRecord.getBizType())) { // 更新商机跟进信息
             businessService.updateBusinessFollowUpBatch(Collections.singletonList(updateFollowUpReqBO));
         }
-        if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_LEADS.getType(), followUpRecord.getBizType())) { // 更新线索跟进信息
-            clueService.updateClueFollowUp(updateFollowUpReqBO);
+        if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CLUE.getType(), followUpRecord.getBizType())) { // 更新线索跟进信息
+            clueService.updateClueFollowUp(followUpRecord.getId(), followUpRecord.getNextTime(), followUpRecord.getContent());
         }
         if (ObjUtil.notEqual(CrmBizTypeEnum.CRM_CONTACT.getType(), followUpRecord.getBizType())) { // 更新联系人跟进信息
             contactService.updateContactFollowUpBatch(Collections.singletonList(updateFollowUpReqBO));

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java

@@ -3,9 +3,11 @@ package cn.iocoder.yudao.module.crm.service.permission.bo;
 import cn.iocoder.yudao.framework.common.validation.InEnum;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
+import lombok.AllArgsConstructor;
 import lombok.Data;
 
 import jakarta.validation.constraints.NotNull;
+import lombok.NoArgsConstructor;
 
 /**
  * 数据权限转移 Request BO
@@ -13,6 +15,8 @@ import jakarta.validation.constraints.NotNull;
  * @author HUIHUI
  */
 @Data
+@NoArgsConstructor
+@AllArgsConstructor
 public class CrmPermissionTransferReqBO {
 
     /**