Ver Fonte

Merge branch 'develop' of https://gitee.com/puhui999/ruoyi-vue-pro into develop

# Conflicts:
#	yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/category/ProductCategoryServiceImplTest.java
YunaiV há 1 ano atrás
pai
commit
d80406acbb
56 ficheiros alterados com 954 adições e 312 exclusões
  1. 6 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java
  2. 6 0
      yudao-module-crm/yudao-module-crm-api/pom.xml
  3. 5 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
  4. 42 3
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java
  5. 18 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/operatelog/CrmParseFunctionNameConstants.java
  6. 10 8
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactSaveReqVO.java
  7. 29 18
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
  8. 2 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
  9. 2 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerDistributeReqVO.java
  10. 6 7
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerSaveReqVO.java
  11. 5 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/limitconfig/CrmCustomerLimitConfigSaveReqVO.java
  12. 94 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java
  13. 37 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordPageReqVO.java
  14. 54 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java
  15. 43 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordSaveReqVO.java
  16. 5 10
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
  17. 4 9
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
  18. 0 16
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
  19. 0 7
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerLimitConfigConvert.java
  20. 4 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/CrmContactDO.java
  21. 4 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
  22. 5 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/followup/CrmFollowUpRecordDO.java
  23. 6 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/CrmContactMapper.java
  24. 6 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerPoolConfigMapper.java
  25. 28 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/followup/CrmFollowUpRecordMapper.java
  26. 7 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
  27. 4 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmContactParseFunction.java
  28. 10 10
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmContractParseFunction.java
  29. 3 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmCustomerIndustryParseFunction.java
  30. 3 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmCustomerLevelParseFunction.java
  31. 4 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmCustomerParseFunction.java
  32. 5 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmCustomerSourceParseFunction.java
  33. 8 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
  34. 47 17
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
  35. 4 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
  36. 23 5
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactService.java
  37. 44 22
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java
  38. 8 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java
  39. 50 18
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
  40. 2 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerLimitConfigServiceImpl.java
  41. 3 5
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
  42. 39 9
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
  43. 54 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordService.java
  44. 70 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java
  45. 37 27
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
  46. 0 9
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java
  47. 30 24
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
  48. 2 10
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
  49. 37 28
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
  50. 2 1
      yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivableServiceImplTest.java
  51. 17 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/operatelog/SysParseFunctionNameConstants.java
  52. 3 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/AdminUserParseFunction.java
  53. 3 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/AreaParseFunction.java
  54. 6 6
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/BooleanParseFunction.java
  55. 3 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/DeptParseFunction.java
  56. 5 5
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/SexParseFunction.java

+ 6 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java

@@ -122,7 +122,12 @@ public class LocalDateTimeUtils {
         return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
     }
 
-    // TODO @puhui999:加下注释哈;
+    /**
+     * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负。
+     *
+     * @param dateTime 日期
+     * @return 相差天数
+     */
     public static Long between(LocalDateTime dateTime) {
         return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS);
     }

+ 6 - 0
yudao-module-crm/yudao-module-crm-api/pom.xml

@@ -28,6 +28,12 @@
             <artifactId>spring-boot-starter-validation</artifactId>
             <optional>true</optional>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-system-api</artifactId>
+            <version>${revision}</version>
+            <optional>true</optional>
+        </dependency>
     </dependencies>
 
 </project>

+ 5 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java

@@ -44,6 +44,7 @@ public interface ErrorCodeConstants {
     ErrorCode CUSTOMER_UNLOCK_FAIL_IS_UNLOCK = new ErrorCode(1_020_006_008, "解锁客户失败,它已经处于未锁定状态");
     ErrorCode CUSTOMER_LOCK_EXCEED_LIMIT = new ErrorCode(1_020_006_009, "锁定客户失败,超出锁定规则上限");
     ErrorCode CUSTOMER_OWNER_EXCEED_LIMIT = new ErrorCode(1_020_006_010, "操作失败,超出客户数拥有上限");
+    ErrorCode CUSTOMER_DELETE_FAIL_HAVE_REFERENCE = new ErrorCode(1_020_006_011, "删除客户失败,有关联{}");
 
     // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
@@ -54,6 +55,7 @@ public interface ErrorCodeConstants {
     ErrorCode CRM_PERMISSION_DELETE_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_005, "删除数据权限失败,原因:存在负责人");
     ErrorCode CRM_PERMISSION_DELETE_DENIED = new ErrorCode(1_020_007_006, "删除数据权限失败,原因:没有权限");
     ErrorCode CRM_PERMISSION_DELETE_SELF_PERMISSION_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_007, "删除数据权限失败,原因:不能删除负责人");
+    ErrorCode CRM_PERMISSION_CREATE_FAIL = new ErrorCode(1_020_007_008, "创建数据权限失败,原因:所加用户已有权限");
 
     // ========== 产品 1_020_008_000 ==========
     ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_020_008_000, "产品不存在");
@@ -77,4 +79,7 @@ public interface ErrorCodeConstants {
     // ========== 客户公海规则设置 1_020_012_000 ==========
     ErrorCode CUSTOMER_LIMIT_CONFIG_NOT_EXISTS = new ErrorCode(1_020_012_000, "客户限制配置不存在");
 
+    // ========== 跟进记录 1_020_013_000 ==========
+    ErrorCode FOLLOW_UP_RECORD_NOT_EXISTS = new ErrorCode(1_020_013_000, "跟进记录不存在");
+
 }

+ 42 - 3
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java

@@ -1,5 +1,8 @@
 package cn.iocoder.yudao.module.crm.enums;
 
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CONTRACT_BY_ID;
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_ADMIN_USER_BY_ID;
+
 /**
  * CRM 操作日志枚举
  * 目的:统一管理,也减少 Service 里各种“复杂”字符串
@@ -22,9 +25,9 @@ public interface LogRecordConstants {
     String CRM_CUSTOMER_DELETE_SUB_TYPE = "删除客户";
     String CRM_CUSTOMER_DELETE_SUCCESS = "删除了客户【{{#customerName}}】";
     String CRM_CUSTOMER_TRANSFER_SUB_TYPE = "转移客户";
-    String CRM_CUSTOMER_TRANSFER_SUCCESS = "将客户【{{#crmCustomer.name}}】的负责人从【{getAdminUserById{#crmCustomer.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
-    String CRM_CUSTOMER_LOCK_SUB_TYPE = "{{#crmCustomer.lockStatus ? '解锁客户' : '锁定客户'}}";
-    String CRM_CUSTOMER_LOCK_SUCCESS = "{{#crmCustomer.lockStatus ? '将客户【' + #crmCustomer.name + '】解锁' : '将客户【' + #crmCustomer.name + '】锁定'}}";
+    String CRM_CUSTOMER_TRANSFER_SUCCESS = "将客户【{{#customer.name}}】的负责人从【{" + GET_ADMIN_USER_BY_ID + "{#customer.ownerUserId}}】变更为了【{" + GET_ADMIN_USER_BY_ID + "{#reqVO.newOwnerUserId}}】";
+    String CRM_CUSTOMER_LOCK_SUB_TYPE = "{{#customer.lockStatus ? '解锁客户' : '锁定客户'}}";
+    String CRM_CUSTOMER_LOCK_SUCCESS = "{{#customer.lockStatus ? '将客户【' + #customer.name + '】解锁' : '将客户【' + #customer.name + '】锁定'}}";
     String CRM_CUSTOMER_POOL_SUB_TYPE = "客户放入公海";
     String CRM_CUSTOMER_POOL_SUCCESS = "将客户【{{#customerName}}】放入了公海";
     String CRM_CUSTOMER_RECEIVE_SUB_TYPE = "{{#ownerUserName != null ? '分配客户' : '领取客户'}}";
@@ -49,14 +52,38 @@ public interface LogRecordConstants {
     // ======================= CRM_CONTACT 联系人 =======================
 
     String CRM_CONTACT_TYPE = "CRM 联系人";
+    String CRM_CONTACT_CREATE_SUB_TYPE = "创建联系人";
+    String CRM_CONTACT_CREATE_SUCCESS = "创建了联系人{{#contact.name}}";
+    String CRM_CONTACT_UPDATE_SUB_TYPE = "更新联系人";
+    String CRM_CONTACT_UPDATE_SUCCESS = "更新了联系人【{{#contactName}}】: {_DIFF{#updateReqVO}}";
+    String CRM_CONTACT_DELETE_SUB_TYPE = "删除联系人";
+    String CRM_CONTACT_DELETE_SUCCESS = "删除了联系人【{{#contactName}}】";
+    String CRM_CONTACT_TRANSFER_SUB_TYPE = "转移联系人";
+    String CRM_CONTACT_TRANSFER_SUCCESS = "将联系人【{{#contact.name}}】的负责人从【{" + GET_ADMIN_USER_BY_ID + "{#contact.ownerUserId}}】变更为了【{" + GET_ADMIN_USER_BY_ID + "{#reqVO.newOwnerUserId}}】";
 
     // ======================= CRM_BUSINESS 商机 =======================
 
     String CRM_BUSINESS_TYPE = "CRM 商机";
+    String CRM_BUSINESS_CREATE_SUB_TYPE = "创建商机";
+    String CRM_BUSINESS_CREATE_SUCCESS = "创建了商机{{#business.name}}";
+    String CRM_BUSINESS_UPDATE_SUB_TYPE = "更新商机";
+    String CRM_BUSINESS_UPDATE_SUCCESS = "更新了商机【{{#businessName}}】: {_DIFF{#updateReqVO}}";
+    String CRM_BUSINESS_DELETE_SUB_TYPE = "删除商机";
+    String CRM_BUSINESS_DELETE_SUCCESS = "删除了商机【{{#businessName}}】";
+    String CRM_BUSINESS_TRANSFER_SUB_TYPE = "转移商机";
+    String CRM_BUSINESS_TRANSFER_SUCCESS = "将商机【{{#business.name}}】的负责人从【{" + GET_ADMIN_USER_BY_ID + "{#business.ownerUserId}}】变更为了【{" + GET_ADMIN_USER_BY_ID + "{#reqVO.newOwnerUserId}}】";
 
     // ======================= CRM_CONTRACT 合同 =======================
 
     String CRM_CONTRACT_TYPE = "CRM 合同";
+    String CRM_CONTRACT_CREATE_SUB_TYPE = "创建合同";
+    String CRM_CONTRACT_CREATE_SUCCESS = "创建了合同{{#contract.name}}";
+    String CRM_CONTRACT_UPDATE_SUB_TYPE = "更新合同";
+    String CRM_CONTRACT_UPDATE_SUCCESS = "更新了合同【{{#contractName}}】: {_DIFF{#updateReqVO}}";
+    String CRM_CONTRACT_DELETE_SUB_TYPE = "删除合同";
+    String CRM_CONTRACT_DELETE_SUCCESS = "删除了合同【{{#contractName}}】";
+    String CRM_CONTRACT_TRANSFER_SUB_TYPE = "转移合同";
+    String CRM_CONTRACT_TRANSFER_SUCCESS = "将合同【{{#contract.name}}】的负责人从【{" + GET_ADMIN_USER_BY_ID + "{#contract.ownerUserId}}】变更为了【{" + GET_ADMIN_USER_BY_ID + "{#reqVO.newOwnerUserId}}】";
 
     // ======================= CRM_PRODUCT 产品 =======================
 
@@ -79,9 +106,21 @@ public interface LogRecordConstants {
     // ======================= CRM_RECEIVABLE 回款 =======================
 
     String CRM_RECEIVABLE_TYPE = "CRM 回款";
+    String CRM_RECEIVABLE_CREATE_SUB_TYPE = "创建回款";
+    String CRM_RECEIVABLE_CREATE_SUCCESS = "创建了合同【{" + GET_CONTRACT_BY_ID + "{#receivable.contractId}}】的第【{{#receivable.period}}】期回款";
+    String CRM_RECEIVABLE_UPDATE_SUB_TYPE = "更新回款";
+    String CRM_RECEIVABLE_UPDATE_SUCCESS = "更新了合同【{" + GET_CONTRACT_BY_ID + "{#receivable.contractId}}】的第【{{#receivable.period}}】期回款: {_DIFF{#updateReqVO}}";
+    String CRM_RECEIVABLE_DELETE_SUB_TYPE = "删除回款";
+    String CRM_RECEIVABLE_DELETE_SUCCESS = "删除了合同【{" + GET_CONTRACT_BY_ID + "{#receivable.contractId}}】的第【{{#receivable.period}}】期回款";
 
     // ======================= CRM_RECEIVABLE_PLAN 回款计划 =======================
 
     String CRM_RECEIVABLE_PLAN_TYPE = "CRM 回款计划";
+    String CRM_RECEIVABLE_PLAN_CREATE_SUB_TYPE = "创建回款计划";
+    String CRM_RECEIVABLE_PLAN_CREATE_SUCCESS = "创建了合同【{" + GET_CONTRACT_BY_ID + "{#receivablePlan.contractId}}】的第【{{#receivablePlan.period}}】期回款计划";
+    String CRM_RECEIVABLE_PLAN_UPDATE_SUB_TYPE = "更新回款计划";
+    String CRM_RECEIVABLE_PLAN_UPDATE_SUCCESS = "更新了合同【{" + GET_CONTRACT_BY_ID + "{#receivablePlan.contractId}}】的第【{{#receivablePlan.period}}】期回款计划: {_DIFF{#updateReqVO}}";
+    String CRM_RECEIVABLE_PLAN_DELETE_SUB_TYPE = "删除回款计划";
+    String CRM_RECEIVABLE_PLAN_DELETE_SUCCESS = "删除了合同【{" + GET_CONTRACT_BY_ID + "{#receivablePlan.contractId}}】的第【{{#receivablePlan.period}}】期回款计划";
 
 }

+ 18 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/operatelog/CrmParseFunctionNameConstants.java

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.enums.operatelog;
+
+/**
+ * functionName 常量枚举
+ * 方便别的模块调用
+ *
+ * @author HUIHUI
+ */
+public interface CrmParseFunctionNameConstants {
+
+    String GET_CONTACT_BY_ID = "getContactById"; // 获取联系人信息
+    String GET_CUSTOMER_BY_ID = "getCustomerById"; // 获取客户信息
+    String GET_CUSTOMER_INDUSTRY = "getCustomerIndustry"; // 获取客户行业信息
+    String GET_CUSTOMER_LEVEL = "getCustomerLevel"; // 获取客户级别
+    String GET_CUSTOMER_SOURCE = "getCustomerSource"; // 获取客户来源
+    String GET_CONTRACT_BY_ID = "getContractById"; // 获取合同信息
+
+}

+ 10 - 8
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactSaveReqVO.java

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import cn.iocoder.yudao.framework.common.validation.Mobile;
 import cn.iocoder.yudao.framework.common.validation.Telephone;
-import cn.iocoder.yudao.module.crm.framework.operatelog.core.*;
 import com.mzt.logapi.starter.annotation.DiffLogField;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.Email;
@@ -14,10 +13,13 @@ import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CONTACT_BY_ID;
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CUSTOMER_BY_ID;
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.*;
 
 @Schema(description = "管理后台 - CRM 联系人创建/更新 Request VO")
 @Data
-public class CrmContactSaveReqVO  {
+public class CrmContactSaveReqVO {
 
     @Schema(description = "主键", example = "3167")
     private Long id;
@@ -28,11 +30,11 @@ public class CrmContactSaveReqVO  {
     private String name;
 
     @Schema(description = "客户编号", example = "10795")
-    @DiffLogField(name = "姓名", function = CrmCustomerParseFunction.NAME)
+    @DiffLogField(name = "姓名", function = GET_CUSTOMER_BY_ID)
     private Long customerId;
 
     @Schema(description = "性别")
-    @DiffLogField(name = "性别", function = CrmSexParseFunction.NAME)
+    @DiffLogField(name = "性别", function = GET_SEX)
     private Integer sex;
 
     @Schema(description = "职位")
@@ -40,11 +42,11 @@ public class CrmContactSaveReqVO  {
     private String post;
 
     @Schema(description = "是否关键决策人")
-    @DiffLogField(name = "关键决策人", function = CrmBooleanParseFunction.NAME)
+    @DiffLogField(name = "关键决策人", function = GET_BOOLEAN)
     private Boolean master;
 
     @Schema(description = "直属上级", example = "23457")
-    @DiffLogField(name = "直属上级", function = CrmContactParseFunction.NAME)
+    @DiffLogField(name = "直属上级", function = GET_CONTACT_BY_ID)
     private Long parentId;
 
     @Schema(description = "手机号", example = "1387171766")
@@ -71,7 +73,7 @@ public class CrmContactSaveReqVO  {
     private String email;
 
     @Schema(description = "地区编号", example = "20158")
-    @DiffLogField(name = "所在地", function = "getAreaById")
+    @DiffLogField(name = "所在地", function = GET_AREA)
     private Integer areaId;
 
     @Schema(description = "地址")
@@ -84,7 +86,7 @@ public class CrmContactSaveReqVO  {
 
     @Schema(description = "负责人用户编号", example = "14334")
     @NotNull(message = "负责人不能为空")
-    @DiffLogField(name = "负责人", function = CrmSysUserParseFunction.NAME)
+    @DiffLogField(name = "负责人", function = GET_ADMIN_USER_BY_ID)
     private Long ownerUserId;
 
     @Schema(description = "最后跟进时间")

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

@@ -1,6 +1,8 @@
 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.date.LocalDateTimeUtils;
@@ -32,6 +34,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
@@ -112,32 +115,40 @@ public class CrmCustomerController {
         }
 
         // 2. 拼接数据
-        Map<Long, Long> poolDayMap = getPoolDayMap(pageResult);  // 距离进入公海的时间
+        Map<Long, Long> poolDayMap = null;
+        if (ObjUtil.notEqual(pageVO.getPool(), Boolean.TRUE)) {
+            poolDayMap = getPoolDayMap(pageResult.getList());  // 距离进入公海的时间
+        }
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                 convertSetByFlatMap(pageResult.getList(), user -> Stream.of(Long.parseLong(user.getCreator()), user.getOwnerUserId())));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
         return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap, poolDayMap));
     }
 
-    // TODO @puhui999:加下注释哈;
-    private Map<Long, Long> getPoolDayMap(PageResult<CrmCustomerDO> pageResult) {
-        Map<Long, Long> poolDayMap = null;
+    /**
+     * 获取距离进入公海的时间
+     *
+     * @param customerList 客户列表
+     * @return Map<key 客户编号, value 距离进入公海的时间>
+     */
+    private Map<Long, Long> getPoolDayMap(List<CrmCustomerDO> customerList) {
         CrmCustomerPoolConfigDO customerPoolConfig = customerPoolConfigService.getCustomerPoolConfig();
-        // TODO @puhui999:if return 减少括号
-        if (customerPoolConfig != null && customerPoolConfig.getEnabled()) { // 有公海配置的情况
-            // TODO @puhui999:item 改成 customer 更好,容易理解;
-            poolDayMap = convertMap(pageResult.getList(), CrmCustomerDO::getId, item -> {
-                long dealExpireDay = 0;
-                if (!item.getDealStatus()) { // 检查是否成交
-                    dealExpireDay = customerPoolConfig.getDealExpireDays() - LocalDateTimeUtils.between(item.getCreateTime());
-                }
-                // TODO @puhui999:需要考虑 contactLastTime 为空的情况哈;
-                long contactExpireDay = customerPoolConfig.getContactExpireDays() - LocalDateTimeUtils.between(item.getContactLastTime());
-                return dealExpireDay == 0 ? contactExpireDay : Math.min(dealExpireDay, contactExpireDay);
-            });
-            // TODO @puhui999:需要考虑 lock 的情况么?
+        if (customerPoolConfig == null || !customerPoolConfig.getEnabled()) {
+            return MapUtil.empty();
         }
-        return poolDayMap;
+        // TODO @puhui999:需要考虑 lock 的情况么? 回复:锁定正常显示距离进入公海的时间有个提示
+        return convertMap(customerList, CrmCustomerDO::getId, customer -> {
+            long dealExpireDay = 0;
+            if (!customer.getDealStatus()) { // 检查是否成交
+                dealExpireDay = customerPoolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getCreateTime());
+            }
+            LocalDateTime lastTime = customer.getContactLastTime() != null ? customer.getContactLastTime() : customer.getCreateTime();
+            long contactExpireDay = customerPoolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime);
+            if (contactExpireDay < 0) {
+                contactExpireDay = 0; // 如果为负的话重置为零
+            }
+            return Math.min(dealExpireDay, contactExpireDay);
+        });
     }
 
     @GetMapping(value = "/list-all-simple")

+ 2 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java

@@ -1,9 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.poolconfig.CrmCustomerPoolConfigRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.poolconfig.CrmCustomerPoolConfigSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerPoolConfigService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -30,7 +30,7 @@ public class CrmCustomerPoolConfigController {
     @PreAuthorize("@ss.hasPermission('crm:customer-pool-config:query')")
     public CommonResult<CrmCustomerPoolConfigRespVO> getCustomerPoolConfig() {
         CrmCustomerPoolConfigDO customerPoolConfig = customerPoolConfigService.getCustomerPoolConfig();
-        return success(CrmCustomerConvert.INSTANCE.convert(customerPoolConfig));
+        return success(BeanUtils.toBean(customerPoolConfig, CrmCustomerPoolConfigRespVO.class));
     }
 
     @PutMapping("/save")

+ 2 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerDistributeReqVO.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
@@ -11,7 +12,7 @@ import java.util.List;
 public class CrmCustomerDistributeReqVO {
 
     @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024]")
-    @NotNull(message = "客户编号不能为空") // TODO @puhui999:list 是 @NotEmpty
+    @NotEmpty(message = "客户编号不能为空")
     private List<Long> ids;
 
     @Schema(description = "负责人", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")

+ 6 - 7
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerSaveReqVO.java

@@ -5,9 +5,6 @@ import cn.iocoder.yudao.framework.common.validation.Mobile;
 import cn.iocoder.yudao.framework.common.validation.Telephone;
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
 import cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLevelEnum;
-import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmIndustryParseFunction;
-import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmLevelParseFunction;
-import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmSourceParseFunction;
 import com.mzt.logapi.starter.annotation.DiffLogField;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.Email;
@@ -20,6 +17,8 @@ import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY;
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.*;
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_AREA;
 
 @Schema(description = "管理后台 - CRM 客户新增/修改 Request VO")
 @Data
@@ -34,17 +33,17 @@ public class CrmCustomerSaveReqVO {
     private String name;
 
     @Schema(description = "所属行业", example = "1")
-    @DiffLogField(name = "所属行业", function = CrmIndustryParseFunction.NAME)
+    @DiffLogField(name = "所属行业", function = GET_CUSTOMER_INDUSTRY)
     @DictFormat(CRM_CUSTOMER_INDUSTRY)
     private Integer industryId;
 
     @Schema(description = "客户等级", example = "2")
-    @DiffLogField(name = "客户等级", function = CrmLevelParseFunction.NAME)
+    @DiffLogField(name = "客户等级", function = GET_CUSTOMER_LEVEL)
     @InEnum(CrmCustomerLevelEnum.class)
     private Integer level;
 
     @Schema(description = "客户来源", example = "3")
-    @DiffLogField(name = "客户来源", function = CrmSourceParseFunction.NAME)
+    @DiffLogField(name = "客户来源", function = GET_CUSTOMER_SOURCE)
     private Integer source;
 
     @Schema(description = "手机", example = "18000000000")
@@ -87,7 +86,7 @@ public class CrmCustomerSaveReqVO {
     private String remark;
 
     @Schema(description = "地区编号", example = "20158")
-    @DiffLogField(name = "地区编号", function = "getAreaById")
+    @DiffLogField(name = "地区编号", function = GET_AREA)
     private Integer areaId;
 
     @Schema(description = "详细地址", example = "北京市海淀区")

+ 5 - 3
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/limitconfig/CrmCustomerLimitConfigSaveReqVO.java

@@ -7,6 +7,9 @@ import lombok.Data;
 
 import java.util.List;
 
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_ADMIN_USER_BY_ID;
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_DEPT_BY_ID;
+
 @Schema(description = "管理后台 - 客户限制配置创建/更新 Request VO")
 @Data
 public class CrmCustomerLimitConfigSaveReqVO {
@@ -19,13 +22,12 @@ public class CrmCustomerLimitConfigSaveReqVO {
     @DiffLogField(name = "规则类型")
     private Integer type;
 
-    // TODO @puhui999:可以把 Function 那的 functionName 搞成 NAME 枚举,这里直接引用。这样后续改动更方便哈。
     @Schema(description = "规则适用人群")
-    @DiffLogField(name = "规则适用人群", function = "getAdminUserById")
+    @DiffLogField(name = "规则适用人群", function = GET_ADMIN_USER_BY_ID)
     private List<Long> userIds;
 
     @Schema(description = "规则适用部门")
-    @DiffLogField(name = "规则适用部门", function = "getDeptById")
+    @DiffLogField(name = "规则适用部门", function = GET_DEPT_BY_ID)
     private List<Long> deptIds;
 
     @Schema(description = "数量上限", requiredMode = Schema.RequiredMode.REQUIRED, example = "28384")

+ 94 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java

@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.crm.controller.admin.followup;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordSaveReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
+import cn.iocoder.yudao.module.crm.service.followup.CrmFollowUpRecordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+
+@Tag(name = "管理后台 - 跟进记录")
+@RestController
+@RequestMapping("/crm/follow-up-record")
+@Validated
+public class CrmFollowUpRecordController {
+
+    @Resource
+    private CrmFollowUpRecordService crmFollowUpRecordService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建跟进记录")
+    @PreAuthorize("@ss.hasPermission('crm:follow-up-record:create')")
+    public CommonResult<Long> createFollowUpRecord(@Valid @RequestBody CrmFollowUpRecordSaveReqVO createReqVO) {
+        return success(crmFollowUpRecordService.createFollowUpRecord(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新跟进记录")
+    @PreAuthorize("@ss.hasPermission('crm:follow-up-record:update')")
+    public CommonResult<Boolean> updateFollowUpRecord(@Valid @RequestBody CrmFollowUpRecordSaveReqVO updateReqVO) {
+        crmFollowUpRecordService.updateFollowUpRecord(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除跟进记录")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:follow-up-record:delete')")
+    public CommonResult<Boolean> deleteFollowUpRecord(@RequestParam("id") Long id) {
+        crmFollowUpRecordService.deleteFollowUpRecord(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得跟进记录")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:follow-up-record:query')")
+    public CommonResult<CrmFollowUpRecordRespVO> getFollowUpRecord(@RequestParam("id") Long id) {
+        CrmFollowUpRecordDO followUpRecord = crmFollowUpRecordService.getFollowUpRecord(id);
+        return success(BeanUtils.toBean(followUpRecord, CrmFollowUpRecordRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得跟进记录分页")
+    @PreAuthorize("@ss.hasPermission('crm:follow-up-record:query')")
+    public CommonResult<PageResult<CrmFollowUpRecordRespVO>> getFollowUpRecordPage(@Valid CrmFollowUpRecordPageReqVO pageReqVO) {
+        PageResult<CrmFollowUpRecordDO> pageResult = crmFollowUpRecordService.getFollowUpRecordPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, CrmFollowUpRecordRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出跟进记录 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:follow-up-record:export')")
+    @OperateLog(type = EXPORT)
+    public void exportFollowUpRecordExcel(@Valid CrmFollowUpRecordPageReqVO pageReqVO,
+                                          HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<CrmFollowUpRecordDO> list = crmFollowUpRecordService.getFollowUpRecordPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "跟进记录.xls", "数据", CrmFollowUpRecordRespVO.class,
+                BeanUtils.toBean(list, CrmFollowUpRecordRespVO.class));
+    }
+
+}

+ 37 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordPageReqVO.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.crm.controller.admin.followup.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 跟进记录分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmFollowUpRecordPageReqVO extends PageParam {
+
+    @Schema(description = "数据类型", example = "2")
+    private Integer bizType;
+
+    @Schema(description = "数据编号", example = "5564")
+    private Long bizId;
+
+    @Schema(description = "跟进类型", example = "2")
+    private Integer type;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] nextTime;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 54 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.crm.controller.admin.followup.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 跟进记录 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class CrmFollowUpRecordRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28800")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("数据类型")
+    private Integer bizType;
+
+    @Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5564")
+    @ExcelProperty("数据编号")
+    private Long bizId;
+
+    @Schema(description = "跟进类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty(value = "跟进类型", converter = DictConvert.class)
+    @DictFormat("crm_follow_up_type") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer type;
+
+    @Schema(description = "跟进内容", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("跟进内容")
+    private String content;
+
+    @Schema(description = "下次联系时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime nextTime;
+
+    @Schema(description = "关联的商机编号数组")
+    @ExcelProperty("关联的商机编号数组")
+    private String businessIds;
+
+    @Schema(description = "关联的联系人编号数组")
+    @ExcelProperty("关联的联系人编号数组")
+    private String contactIds;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 43 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordSaveReqVO.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.crm.controller.admin.followup.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 跟进记录新增/修改 Request VO")
+@Data
+public class CrmFollowUpRecordSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28800")
+    private Long id;
+
+    @Schema(description = "数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "数据类型不能为空")
+    private Integer bizType;
+
+    @Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5564")
+    @NotNull(message = "数据编号不能为空")
+    private Long bizId;
+
+    @Schema(description = "跟进类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "跟进类型不能为空")
+    private Integer type;
+
+    @Schema(description = "跟进内容", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "跟进内容不能为空")
+    private String content;
+
+    @Schema(description = "下次联系时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "下次联系时间不能为空")
+    private LocalDateTime nextTime;
+
+    @Schema(description = "关联的商机编号数组")
+    private String businessIds;
+
+    @Schema(description = "关联的联系人编号数组")
+    private String contactIds;
+
+}

+ 5 - 10
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java

@@ -7,7 +7,10 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.*;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivableConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
@@ -59,7 +62,7 @@ public class CrmReceivableController {
     @Operation(summary = "创建回款")
     @PreAuthorize("@ss.hasPermission('crm:receivable:create')")
     public CommonResult<Long> createReceivable(@Valid @RequestBody CrmReceivableCreateReqVO createReqVO) {
-        return success(receivableService.createReceivable(createReqVO));
+        return success(receivableService.createReceivable(createReqVO, getLoginUserId()));
     }
 
     @PutMapping("/update")
@@ -141,12 +144,4 @@ public class CrmReceivableController {
         return CrmReceivableConvert.INSTANCE.convertPage(pageResult, userMap, customerList, contractList);
     }
 
-    @PutMapping("/transfer")
-    @Operation(summary = "回款转移")
-    @PreAuthorize("@ss.hasPermission('crm:receivable:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmReceivableTransferReqVO reqVO) {
-        receivableService.transferReceivable(reqVO, getLoginUserId());
-        return success(true);
-    }
-
 }

+ 4 - 9
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java

@@ -7,7 +7,10 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.*;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivablePlanConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
@@ -148,12 +151,4 @@ public class CrmReceivablePlanController {
         return CrmReceivablePlanConvert.INSTANCE.convertPage(pageResult, userMap, customerList, contractList, receivableList);
     }
 
-    @PutMapping("/transfer")
-    @Operation(summary = "回款计划转移")
-    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmReceivablePlanTransferReqVO reqVO) {
-        receivablePlanService.transferReceivablePlan(reqVO, getLoginUserId());
-        return success(true);
-    }
-
 }

+ 0 - 16
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java

@@ -3,13 +3,8 @@ package cn.iocoder.yudao.module.crm.convert.customer;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerSaveReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerTransferReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.poolconfig.CrmCustomerPoolConfigRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.poolconfig.CrmCustomerPoolConfigSaveReqVO;
-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.dal.dataobject.customer.CrmCustomerPoolConfigDO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -31,9 +26,6 @@ public interface CrmCustomerConvert {
 
     CrmCustomerConvert INSTANCE = Mappers.getMapper(CrmCustomerConvert.class);
 
-    // TODO @puhui999:可以清理掉可以用 BeanUtil 替代的方法哈
-    CrmCustomerDO convert(CrmCustomerSaveReqVO bean);
-
     CrmCustomerRespVO convert(CrmCustomerDO bean);
 
     /**
@@ -74,12 +66,4 @@ public interface CrmCustomerConvert {
         return result;
     }
 
-    CrmCustomerPoolConfigRespVO convert(CrmCustomerPoolConfigDO customerPoolConfig);
-
-    CrmCustomerPoolConfigDO convert(CrmCustomerPoolConfigSaveReqVO updateReqVO);
-
-    // TODO @min:使用 BeanUtils 拷贝哈。我们慢慢简单的对象,不再直接基于 convert 做啦。
-    @Mapping(ignore = true, target = "id")
-    CrmCustomerSaveReqVO convert(CrmClueDO bean);
-
 }

+ 0 - 7
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerLimitConfigConvert.java

@@ -3,14 +3,12 @@ package cn.iocoder.yudao.module.crm.convert.customer;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigSaveReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -23,13 +21,8 @@ public interface CrmCustomerLimitConfigConvert {
 
     CrmCustomerLimitConfigConvert INSTANCE = Mappers.getMapper(CrmCustomerLimitConfigConvert.class);
 
-    // TODO @puhui999:可以把 convert 改成 BeanUtils
-    CrmCustomerLimitConfigDO convert(CrmCustomerLimitConfigSaveReqVO bean);
-
     CrmCustomerLimitConfigRespVO convert(CrmCustomerLimitConfigDO bean);
 
-    List<CrmCustomerLimitConfigRespVO> convertList(List<CrmCustomerLimitConfigDO> list);
-
     PageResult<CrmCustomerLimitConfigRespVO> convertPage(PageResult<CrmCustomerLimitConfigDO> page);
 
     default PageResult<CrmCustomerLimitConfigRespVO> convertPage(PageResult<CrmCustomerLimitConfigDO> pageResult,

+ 4 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/CrmContactDO.java

@@ -105,7 +105,10 @@ public class CrmContactDO extends BaseDO {
      * 最后跟进时间
      */
     private LocalDateTime contactLastTime;
-    // TODO @puhui999:增加一个字段 contactLastContent;最后跟进内容
+    /**
+     * 最后跟进内容
+     */
+    private String contactLastContent;
     /**
      * 下次联系时间
      */

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

@@ -118,7 +118,10 @@ public class CrmCustomerDO extends BaseDO {
      * 最后跟进时间
      */
     private LocalDateTime contactLastTime;
-    // TODO @puhui999:增加一个字段 contactLastContent;最后跟进内容
+    /**
+     * 最后跟进内容
+     */
+    private String contactLastContent;
     /**
      * 下次联系时间
      */

+ 5 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/followup/CrmFollowUpRecordDO.java

@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.followup;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
@@ -21,7 +23,7 @@ import java.util.List;
  *
  * @author 芋道源码
  */
-@TableName(value = "crm_follow_up_record")
+@TableName(value = "crm_follow_up_record", autoResultMap = true)
 @KeySequence("crm_follow_up_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -70,12 +72,14 @@ public class CrmFollowUpRecordDO extends BaseDO {
      *
      * 关联 {@link CrmBusinessDO#getId()}
      */
+    @TableField(typeHandler = LongListTypeHandler.class)
     private List<Long> businessIds;
     /**
      * 关联的联系人编号数组
      *
      * 关联 {@link CrmContactDO#getId()}
      */
+    @TableField(typeHandler = LongListTypeHandler.class)
     private List<Long> contactIds;
 
 }

+ 6 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/CrmContactMapper.java

@@ -28,6 +28,12 @@ public interface CrmContactMapper extends BaseMapperX<CrmContactDO> {
                 .set(CrmContactDO::getOwnerUserId, ownerUserId));
     }
 
+    default int updateOwnerUserIdByCustomerId(Long customerId, Long ownerUserId) {
+        return update(new LambdaUpdateWrapper<CrmContactDO>()
+                .eq(CrmContactDO::getCustomerId, customerId)
+                .set(CrmContactDO::getOwnerUserId, ownerUserId));
+    }
+
     default PageResult<CrmContactDO> selectPageByCustomerId(CrmContactPageReqVO pageVO) {
         return selectPage(pageVO, new LambdaQueryWrapperX<CrmContactDO>()
                 .eq(CrmContactDO::getCustomerId, pageVO.getCustomerId()) // 指定客户编号

+ 6 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerPoolConfigMapper.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.customer;
 
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -11,4 +12,9 @@ import org.apache.ibatis.annotations.Mapper;
  */
 @Mapper
 public interface CrmCustomerPoolConfigMapper extends BaseMapperX<CrmCustomerPoolConfigDO> {
+
+    default CrmCustomerPoolConfigDO selectOne() {
+        return selectOne(new LambdaQueryWrapperX<CrmCustomerPoolConfigDO>().last("LIMIT 1"));
+    }
+
 }

+ 28 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/followup/CrmFollowUpRecordMapper.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.followup;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 跟进记录 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface CrmFollowUpRecordMapper extends BaseMapperX<CrmFollowUpRecordDO> {
+
+    default PageResult<CrmFollowUpRecordDO> selectPage(CrmFollowUpRecordPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmFollowUpRecordDO>()
+                .eqIfPresent(CrmFollowUpRecordDO::getBizType, reqVO.getBizType())
+                .eqIfPresent(CrmFollowUpRecordDO::getBizId, reqVO.getBizId())
+                .eqIfPresent(CrmFollowUpRecordDO::getType, reqVO.getType())
+                .betweenIfPresent(CrmFollowUpRecordDO::getNextTime, reqVO.getNextTime())
+                .betweenIfPresent(CrmFollowUpRecordDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmFollowUpRecordDO::getId));
+    }
+
+}

+ 7 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java

@@ -59,4 +59,11 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 .eq(CrmPermissionDO::getBizId, bizId));
     }
 
+    default Long selectListByBiz(Collection<Integer> bizTypes, Collection<Long> bizIds, Collection<Long> userIds) {
+        return selectCount(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .in(CrmPermissionDO::getBizType, bizTypes)
+                .in(CrmPermissionDO::getBizId, bizIds)
+                .in(CrmPermissionDO::getUserId, userIds));
+    }
+
 }

+ 4 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmContactParseFunction.java

@@ -8,8 +8,10 @@ import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CONTACT_BY_ID;
+
 /**
- * 行业的 {@link IParseFunction} 实现类
+ * CRM 联系人的 {@link IParseFunction} 实现类
  *
  * @author HUIHUI
  */
@@ -17,8 +19,6 @@ import org.springframework.stereotype.Component;
 @Slf4j
 public class CrmContactParseFunction implements IParseFunction {
 
-    public static final String NAME = "getContactById";
-
     @Resource
     private CrmContactService contactService;
 
@@ -29,7 +29,7 @@ public class CrmContactParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return NAME;
+        return GET_CONTACT_BY_ID;
     }
 
     @Override

+ 10 - 10
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmSysUserParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmContractParseFunction.java

@@ -1,26 +1,26 @@
 package cn.iocoder.yudao.module.crm.framework.operatelog.core;
 
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
-import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
+import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import com.mzt.logapi.service.IParseFunction;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CONTRACT_BY_ID;
+
 /**
- * 行业的 {@link IParseFunction} 实现类
+ * CRM 合同的 {@link IParseFunction} 实现类
  *
  * @author HUIHUI
  */
 @Component
 @Slf4j
-public class CrmSysUserParseFunction implements IParseFunction {
-
-    public static final String NAME = "getUserById";
+public class CrmContractParseFunction implements IParseFunction {
 
     @Resource
-    private AdminUserApi adminUserApi;
+    private CrmContractService contractService;
 
     @Override
     public boolean executeBefore() {
@@ -29,7 +29,7 @@ public class CrmSysUserParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return NAME;
+        return GET_CONTRACT_BY_ID;
     }
 
     @Override
@@ -37,8 +37,8 @@ public class CrmSysUserParseFunction implements IParseFunction {
         if (StrUtil.isEmptyIfStr(value)) {
             return "";
         }
-        AdminUserRespDTO adminUserRespDTO = adminUserApi.getUser(Long.parseLong(value.toString()));
-        return adminUserRespDTO == null ? "" : adminUserRespDTO.getNickname();
+        CrmContractDO contract = contractService.getContract(Long.parseLong(value.toString()));
+        return contract == null ? "" : contract.getName();
     }
 
 }

+ 3 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmIndustryParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmCustomerIndustryParseFunction.java

@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY;
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CUSTOMER_INDUSTRY;
 
 /**
  * 行业的 {@link IParseFunction} 实现类
@@ -15,9 +16,7 @@ import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_I
  */
 @Component
 @Slf4j
-public class CrmIndustryParseFunction implements IParseFunction {
-
-    public static final String NAME = "getIndustryById";
+public class CrmCustomerIndustryParseFunction implements IParseFunction {
 
     @Override
     public boolean executeBefore() {
@@ -26,7 +25,7 @@ public class CrmIndustryParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return NAME;
+        return GET_CUSTOMER_INDUSTRY;
     }
 
     @Override

+ 3 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmLevelParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmCustomerLevelParseFunction.java

@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL;
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CUSTOMER_LEVEL;
 
 /**
  * 客户等级的 {@link IParseFunction} 实现类
@@ -15,9 +16,7 @@ import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_L
  */
 @Component
 @Slf4j
-public class CrmLevelParseFunction implements IParseFunction {
-
-    public static final String NAME = "getLevel";
+public class CrmCustomerLevelParseFunction implements IParseFunction {
 
     @Override
     public boolean executeBefore() {
@@ -26,7 +25,7 @@ public class CrmLevelParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return NAME;
+        return GET_CUSTOMER_LEVEL;
     }
 
     @Override

+ 4 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmCustomerParseFunction.java

@@ -8,8 +8,10 @@ import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CUSTOMER_BY_ID;
+
 /**
- * 行业的 {@link IParseFunction} 实现类
+ * CRM 客户的 {@link IParseFunction} 实现类
  *
  * @author HUIHUI
  */
@@ -17,8 +19,6 @@ import org.springframework.stereotype.Component;
 @Slf4j
 public class CrmCustomerParseFunction implements IParseFunction {
 
-    public static final String NAME = "getCustomerById";
-
     @Resource
     private CrmCustomerService customerService;
 
@@ -29,7 +29,7 @@ public class CrmCustomerParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return NAME;
+        return GET_CUSTOMER_BY_ID;
     }
 
     @Override

+ 5 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmSourceParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmCustomerSourceParseFunction.java

@@ -7,17 +7,18 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE;
+import static cn.iocoder.yudao.module.crm.enums.operatelog.CrmParseFunctionNameConstants.GET_CUSTOMER_SOURCE;
 
 /**
- * 客户来源的 {@link IParseFunction} 实现类
+ * CRM 客户来源的 {@link IParseFunction} 实现类
  *
  * @author HUIHUI
  */
 @Component
 @Slf4j
-public class CrmSourceParseFunction implements IParseFunction {
+public class CrmCustomerSourceParseFunction implements IParseFunction {
 
-    public static final String NAME = "getSource";
+    public static final String NAME = "getCustomerSource";
 
     @Override
     public boolean executeBefore() {
@@ -26,7 +27,7 @@ public class CrmSourceParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return NAME;
+        return GET_CUSTOMER_SOURCE;
     }
 
     @Override

+ 8 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java

@@ -98,4 +98,12 @@ public interface CrmBusinessService {
      */
     void transferBusiness(CrmBusinessTransferReqVO reqVO, Long userId);
 
+    /**
+     * 获取关联客户的商机数量
+     *
+     * @param customerId 客户编号
+     * @return 数量
+     */
+    Long getBusinessCountByCustomerId(Long customerId);
+
 }

+ 47 - 17
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.service.business;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 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.business.vo.business.CrmBusinessCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
@@ -17,6 +18,9 @@ import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPerm
 import cn.iocoder.yudao.module.crm.service.contact.CrmContactBusinessService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import com.mzt.logapi.context.LogRecordContext;
+import com.mzt.logapi.service.impl.DiffParseFunction;
+import com.mzt.logapi.starter.annotation.LogRecord;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -28,6 +32,7 @@ import java.util.List;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
 
 /**
  * 商机 Service 实现类
@@ -48,7 +53,8 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    // TODO @商机待定:操作日志;
+    @LogRecord(type = CRM_BUSINESS_TYPE, subType = CRM_BUSINESS_CREATE_SUB_TYPE, bizNo = "{{#business.id}}",
+            success = CRM_BUSINESS_CREATE_SUCCESS)
     public Long createBusiness(CrmBusinessCreateReqVO createReqVO, Long userId) {
         // 1. 插入商机
         CrmBusinessDO business = CrmBusinessConvert.INSTANCE.convert(createReqVO);
@@ -60,17 +66,20 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         // 2. 创建数据权限
         permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType())
                 .setBizId(business.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+
+        // 4. 记录操作日志上下文
+        LogRecordContext.putVariable("business", business);
         return business.getId();
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, bizId = "#updateReqVO.id",
-            level = CrmPermissionLevelEnum.WRITE)
-    // TODO @商机待定:操作日志;
+    @LogRecord(type = CRM_BUSINESS_TYPE, subType = CRM_BUSINESS_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
+            success = CRM_BUSINESS_UPDATE_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
     public void updateBusiness(CrmBusinessUpdateReqVO updateReqVO) {
         // 1. 校验存在
-        validateBusinessExists(updateReqVO.getId());
+        CrmBusinessDO oldBusiness = validateBusinessExists(updateReqVO.getId());
 
         // 2. 更新商机
         CrmBusinessDO updateObj = CrmBusinessConvert.INSTANCE.convert(updateReqVO);
@@ -78,20 +87,28 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         // TODO 商机待定:插入商机与产品的关联表;校验商品存在
 
         // TODO @商机待定:如果状态发生变化,插入商机状态变更记录表
+        // 3. 记录操作日志上下文
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessUpdateReqVO.class));
+        LogRecordContext.putVariable("businessName", oldBusiness.getName());
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_BUSINESS_TYPE, subType = CRM_BUSINESS_DELETE_SUB_TYPE, bizNo = "{{#id}}",
+            success = CRM_BUSINESS_DELETE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteBusiness(Long id) {
         // 校验存在
-        validateBusinessExists(id);
+        CrmBusinessDO business = validateBusinessExists(id);
         // TODO @商机待定:需要校验有没关联合同。CrmContractDO 的 businessId 字段
 
         // 删除
         businessMapper.deleteById(id);
         // 删除数据权限
         permissionService.deletePermission(CrmBizTypeEnum.CRM_BUSINESS.getType(), id);
+
+        // 记录操作日志上下文
+        LogRecordContext.putVariable("businessName", business.getName());
     }
 
     private CrmBusinessDO validateBusinessExists(Long id) {
@@ -102,6 +119,28 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         return crmBusiness;
     }
 
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_BUSINESS_TYPE, subType = CRM_BUSINESS_TRANSFER_SUB_TYPE, bizNo = "{{#reqVO.id}}",
+            success = CRM_BUSINESS_TRANSFER_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, bizId = "#reqVO.id", level = CrmPermissionLevelEnum.OWNER)
+    public void transferBusiness(CrmBusinessTransferReqVO reqVO, Long userId) {
+        // 1 校验商机是否存在
+        CrmBusinessDO business = validateBusinessExists(reqVO.getId());
+
+        // 2.1 数据权限转移
+        permissionService.transferPermission(
+                CrmBusinessConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()));
+        // 2.2 设置新的负责人
+        businessMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
+
+        // 记录操作日志上下文
+        LogRecordContext.putVariable("business", business);
+    }
+
+    //======================= 查询相关 =======================
+
     @Override
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, bizId = "#id", level = CrmPermissionLevelEnum.READ)
     public CrmBusinessDO getBusiness(Long id) {
@@ -142,17 +181,8 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    // TODO @puhui999:操作日志
-    public void transferBusiness(CrmBusinessTransferReqVO reqVO, Long userId) {
-        // 1 校验商机是否存在
-        validateBusinessExists(reqVO.getId());
-
-        // 2.1 数据权限转移
-        permissionService.transferPermission(
-                CrmBusinessConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()));
-        // 2.2 设置新的负责人
-        businessMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
+    public Long getBusinessCountByCustomerId(Long customerId) {
+        return businessMapper.selectCount(CrmBusinessDO::getCustomerId, customerId);
     }
 
 }

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

@@ -3,12 +3,13 @@ package cn.iocoder.yudao.module.crm.service.clue;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 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.clue.vo.CrmClueTransformReqVO;
+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.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
@@ -139,8 +140,9 @@ public class CrmClueServiceImpl implements CrmClueService {
 
         // 遍历线索,创建对应的客户
         clues.forEach(clue -> {
+            clue.setId(null);
             // 创建客户
-            customerService.createCustomer(CrmCustomerConvert.INSTANCE.convert(clue), userId);
+            customerService.createCustomer(BeanUtils.toBean(clue, CrmCustomerSaveReqVO.class), userId);
             // 更新线索状态
             // TODO @min:新建一个 CrmClueDO 去更新。尽量规避直接用原本的对象去更新。因为这样万一并发更新,会存在覆盖的问题。
             // TODO @puhui999:如果有跟进记录,需要一起转过去;

+ 23 - 5
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactService.java

@@ -1,7 +1,9 @@
 package cn.iocoder.yudao.module.crm.service.contact;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactTransferReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import jakarta.validation.Valid;
@@ -39,6 +41,22 @@ public interface CrmContactService {
      */
     void deleteContact(Long id);
 
+    /**
+     * 联系人转移
+     *
+     * @param reqVO  请求
+     * @param userId 用户编号
+     */
+    void transferContact(CrmContactTransferReqVO reqVO, Long userId);
+
+    /**
+     * 更新客户联系人负责人
+     *
+     * @param customerId  客户编号
+     * @param ownerUserId 用户编号
+     */
+    void updateOwnerUserIdByCustomerId(Long customerId, Long ownerUserId);
+
     /**
      * 获得联系人
      *
@@ -85,11 +103,11 @@ public interface CrmContactService {
     PageResult<CrmContactDO> getContactPageByCustomerId(CrmContactPageReqVO pageVO);
 
     /**
-     * 联系人转移
+     * 获取关联客户的联系人数量
      *
-     * @param reqVO  请求
-     * @param userId 用户编号
+     * @param customerId 客户编号
+     * @return 数量
      */
-    void transferContact(CrmContactTransferReqVO reqVO, Long userId);
+    Long getContactCountByCustomerId(Long customerId);
 
 }

+ 44 - 22
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java

@@ -33,7 +33,7 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.CRM_CONTACT_TYPE;
+import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_NOT_EXISTS;
 import static java.util.Collections.singletonList;
 
@@ -65,7 +65,8 @@ public class CrmContactServiceImpl implements CrmContactService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @LogRecord(type = CRM_CONTACT_TYPE, subType = "创建联系人", bizNo = "{{#contactId}}", success = "创建了联系人[{{#contactName}}]")
+    @LogRecord(type = CRM_CONTACT_TYPE, subType = CRM_CONTACT_CREATE_SUB_TYPE, bizNo = "{{#contact.id}}",
+            success = CRM_CONTACT_CREATE_SUCCESS)
     public Long createContact(CrmContactSaveReqVO createReqVO, Long userId) {
         // 1. 校验
         validateRelationDataExists(createReqVO);
@@ -86,18 +87,18 @@ public class CrmContactServiceImpl implements CrmContactService {
         }
 
         // 5. 记录操作日志
-        LogRecordContext.putVariable("contactId", contact.getId());
-        LogRecordContext.putVariable("contactName", contact.getName());
+        LogRecordContext.putVariable("contact", contact);
         return contact.getId();
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @LogRecord(type = CRM_CONTACT_TYPE, subType = "更新联系人", bizNo = "{{#updateReqVO.id}}", success = "更新了联系人{_DIFF{#updateReqVO}}")
+    @LogRecord(type = CRM_CONTACT_TYPE, subType = CRM_CONTACT_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
+            success = CRM_CONTACT_UPDATE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
     public void updateContact(CrmContactSaveReqVO updateReqVO) {
         // 1. 校验存在
-        CrmContactDO contactDO = validateContactExists(updateReqVO.getId());
+        CrmContactDO oldContact = validateContactExists(updateReqVO.getId());
         validateRelationDataExists(updateReqVO);
 
         // 2. 更新联系人
@@ -105,7 +106,8 @@ public class CrmContactServiceImpl implements CrmContactService {
         contactMapper.updateById(updateObj);
 
         // 3. 记录操作日志
-        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(contactDO, CrmContactSaveReqVO.class));
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class));
+        LogRecordContext.putVariable("contactName", oldContact.getName());
     }
 
     /**
@@ -133,11 +135,13 @@ public class CrmContactServiceImpl implements CrmContactService {
     }
 
     @Override
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_CONTACT_TYPE, subType = CRM_CONTACT_DELETE_SUB_TYPE, bizNo = "{{#id}}",
+            success = CRM_CONTACT_DELETE_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteContact(Long id) {
         // 1.1 校验存在
-        validateContactExists(id);
+        CrmContactDO contact = validateContactExists(id);
         // 1.2 校验是否关联合同
         if (contractService.getContractCountByContactId(id) > 0) {
             throw exception(CONTACT_DELETE_FAIL_CONTRACT_LINK_EXISTS);
@@ -151,6 +155,9 @@ public class CrmContactServiceImpl implements CrmContactService {
         // 4.2 删除商机关联
         contactBusinessService.deleteContactBusinessByContactId(id);
         // TODO @puhui999:删除跟进记录
+
+        // 记录操作日志上下文
+        LogRecordContext.putVariable("contactName", contact.getName());
     }
 
     private CrmContactDO validateContactExists(Long id) {
@@ -161,6 +168,32 @@ public class CrmContactServiceImpl implements CrmContactService {
         return contactDO;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_CONTACT_TYPE, subType = CRM_CONTACT_TRANSFER_SUB_TYPE, bizNo = "{{#reqVO.id}}",
+            success = CRM_CONTACT_TRANSFER_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#reqVO.id", level = CrmPermissionLevelEnum.OWNER)
+    public void transferContact(CrmContactTransferReqVO reqVO, Long userId) {
+        // 1 校验联系人是否存在
+        CrmContactDO contact = validateContactExists(reqVO.getId());
+
+        // 2.1 数据权限转移
+        permissionService.transferPermission(
+                CrmContactConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTACT.getType()));
+        // 2.2 设置新的负责人
+        contactMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
+
+        // 3. 记录转移日志
+        LogRecordContext.putVariable("contact", contact);
+    }
+
+    @Override
+    public void updateOwnerUserIdByCustomerId(Long customerId, Long ownerUserId) {
+        contactMapper.updateOwnerUserIdByCustomerId(customerId, ownerUserId);
+    }
+
+    //======================= 查询相关 =======================
+
     @Override
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#id", level = CrmPermissionLevelEnum.READ)
     public CrmContactDO getContact(Long id) {
@@ -192,19 +225,8 @@ public class CrmContactServiceImpl implements CrmContactService {
     }
 
     @Override
-    // TODO @puhui999:权限校验
-    // TODO @puhui999:记录操作日志;将联系人【名字】转移给【新负责人】
-    public void transferContact(CrmContactTransferReqVO reqVO, Long userId) {
-        // 1 校验联系人是否存在
-        validateContactExists(reqVO.getId());
-
-        // 2.1 数据权限转移
-        permissionService.transferPermission(
-                CrmContactConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTACT.getType()));
-        // 2.2 设置新的负责人
-        contactMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
-
-        // 3. TODO 记录转移日志
+    public Long getContactCountByCustomerId(Long customerId) {
+        return contactMapper.selectCount(CrmContactDO::getCustomerId, customerId);
     }
 
 }

+ 8 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java

@@ -95,4 +95,12 @@ public interface CrmContractService {
      */
     Long getContractCountByContactId(Long contactId);
 
+    /**
+     * 获取关联客户的合同数量
+     *
+     * @param customerId 客户编号
+     * @return 数量
+     */
+    Long getContractCountByCustomerId(Long customerId);
+
 }

+ 50 - 18
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.service.contract;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 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.contract.vo.CrmContractCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractTransferReqVO;
@@ -15,6 +16,9 @@ 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.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import com.mzt.logapi.context.LogRecordContext;
+import com.mzt.logapi.service.impl.DiffParseFunction;
+import com.mzt.logapi.starter.annotation.LogRecord;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -25,6 +29,7 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTRACT_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
 
 /**
  * CRM 合同 Service 实现类
@@ -42,7 +47,9 @@ public class CrmContractServiceImpl implements CrmContractService {
     private CrmPermissionService crmPermissionService;
 
     @Override
-    // TODO @puhui999:添加操作日志
+    @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_CREATE_SUB_TYPE, bizNo = "{{#contract.id}}",
+            success = CRM_CONTRACT_CREATE_SUCCESS)
     public Long createContract(CrmContractCreateReqVO createReqVO, Long userId) {
         // TODO @合同待定:插入合同商品;需要搞个 BusinessProductDO
         // 插入合同
@@ -53,38 +60,52 @@ public class CrmContractServiceImpl implements CrmContractService {
         crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(userId)
                 .setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()).setBizId(contract.getId())
                 .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+
+        // 4. 记录操作日志上下文
+        LogRecordContext.putVariable("contract", contract);
         return contract.getId();
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
+            success = CRM_CONTRACT_UPDATE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
-    // TODO @puhui999:添加操作日志
     public void updateContract(CrmContractUpdateReqVO updateReqVO) {
         // TODO @合同待定:只有草稿、审批中,可以编辑;
         // 校验存在
-        validateContractExists(updateReqVO.getId());
+        CrmContractDO oldContract = validateContractExists(updateReqVO.getId());
         // 更新合同
         CrmContractDO updateObj = CrmContractConvert.INSTANCE.convert(updateReqVO);
         contractMapper.updateById(updateObj);
         // TODO @合同待定:插入合同商品;需要搞个 BusinessProductDO
+
+        // 3. 记录操作日志上下文
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContract, CrmContractUpdateReqVO.class));
+        LogRecordContext.putVariable("contractName", oldContract.getName());
     }
 
     // TODO @合同待定:缺一个取消合同的接口;只有草稿、审批中可以取消;CrmAuditStatusEnum
 
     // TODO @合同待定:缺一个发起审批的接口;只有草稿可以发起审批;CrmAuditStatusEnum
 
+
     @Override
     @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_DELETE_SUB_TYPE, bizNo = "{{#id}}",
+            success = CRM_CONTRACT_DELETE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteContract(Long id) {
         // TODO @合同待定:如果被 CrmReceivableDO 所使用,则不允许删除
         // 校验存在
-        validateContractExists(id);
+        CrmContractDO contract = validateContractExists(id);
         // 删除
         contractMapper.deleteById(id);
         // 删除数据权限
         crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_CONTRACT.getType(), id);
+
+        // 记录操作日志上下文
+        LogRecordContext.putVariable("contractName", contract.getName());
     }
 
     private CrmContractDO validateContractExists(Long id) {
@@ -95,6 +116,27 @@ public class CrmContractServiceImpl implements CrmContractService {
         return contract;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_TRANSFER_SUB_TYPE, bizNo = "{{#reqVO.id}}",
+            success = CRM_CONTRACT_TRANSFER_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#reqVO.id", level = CrmPermissionLevelEnum.OWNER)
+    public void transferContract(CrmContractTransferReqVO reqVO, Long userId) {
+        // 1. 校验合同是否存在
+        CrmContractDO contract = validateContractExists(reqVO.getId());
+
+        // 2.1 数据权限转移
+        crmPermissionService.transferPermission(
+                CrmContractConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()));
+        // 2.2 设置负责人
+        contractMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
+
+        // 3. 记录转移日志
+        LogRecordContext.putVariable("contract", contract);
+    }
+
+    //======================= 查询相关 =======================
+
     @Override
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#id", level = CrmPermissionLevelEnum.READ)
     public CrmContractDO getContract(Long id) {
@@ -121,23 +163,13 @@ public class CrmContractServiceImpl implements CrmContractService {
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    // 3. TODO @puhui999:记录转移日志
-    // TODO @puhui999:权限校验,这里要搞哇?
-    public void transferContract(CrmContractTransferReqVO reqVO, Long userId) {
-        // 1. 校验合同是否存在
-        validateContractExists(reqVO.getId());
-
-        // 2.1 数据权限转移
-        crmPermissionService.transferPermission(
-                CrmContractConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()));
-        // 2.2 设置负责人
-        contractMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
+    public Long getContractCountByContactId(Long contactId) {
+        return contractMapper.selectCountByContactId(contactId);
     }
 
     @Override
-    public Long getContractCountByContactId(Long contactId) {
-        return contractMapper.selectCountByContactId(contactId);
+    public Long getContractCountByCustomerId(Long customerId) {
+        return contractMapper.selectCount(CrmContractDO::getCustomerId, customerId);
     }
 
     // TODO @合同待定:需要新增一个 ContractConfigDO 表,合同配置,重点是到期提醒;

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

@@ -5,7 +5,6 @@ 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.customer.vo.limitconfig.CrmCustomerLimitConfigPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerLimitConfigConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerLimitConfigMapper;
 import cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLimitConfigTypeEnum;
@@ -49,7 +48,7 @@ public class CrmCustomerLimitConfigServiceImpl implements CrmCustomerLimitConfig
     public Long createCustomerLimitConfig(CrmCustomerLimitConfigSaveReqVO createReqVO) {
         validateUserAndDept(createReqVO.getUserIds(), createReqVO.getDeptIds());
         // 插入
-        CrmCustomerLimitConfigDO customerLimitConfig = CrmCustomerLimitConfigConvert.INSTANCE.convert(createReqVO);
+        CrmCustomerLimitConfigDO customerLimitConfig = BeanUtils.toBean(createReqVO, CrmCustomerLimitConfigDO.class);
         customerLimitConfigMapper.insert(customerLimitConfig);
 
         // 记录操作日志上下文
@@ -66,7 +65,7 @@ public class CrmCustomerLimitConfigServiceImpl implements CrmCustomerLimitConfig
         CrmCustomerLimitConfigDO oldLimitConfig = validateCustomerLimitConfigExists(updateReqVO.getId());
         validateUserAndDept(updateReqVO.getUserIds(), updateReqVO.getDeptIds());
         // 更新
-        CrmCustomerLimitConfigDO updateObj = CrmCustomerLimitConfigConvert.INSTANCE.convert(updateReqVO);
+        CrmCustomerLimitConfigDO updateObj = BeanUtils.toBean(updateReqVO, CrmCustomerLimitConfigDO.class);
         customerLimitConfigMapper.updateById(updateObj);
 
         // 记录操作日志上下文

+ 3 - 5
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java

@@ -1,8 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.poolconfig.CrmCustomerPoolConfigSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerPoolConfigMapper;
 import com.mzt.logapi.context.LogRecordContext;
@@ -34,8 +33,7 @@ public class CrmCustomerPoolConfigServiceImpl implements CrmCustomerPoolConfigSe
      */
     @Override
     public CrmCustomerPoolConfigDO getCustomerPoolConfig() {
-        // TODO @puhui999:这个要搞到 mapper 里噢。
-        return customerPoolConfigMapper.selectOne(new LambdaQueryWrapperX<CrmCustomerPoolConfigDO>().last("LIMIT 1"));
+        return customerPoolConfigMapper.selectOne();
     }
 
     /**
@@ -49,7 +47,7 @@ public class CrmCustomerPoolConfigServiceImpl implements CrmCustomerPoolConfigSe
     public void saveCustomerPoolConfig(CrmCustomerPoolConfigSaveReqVO saveReqVO) {
         // 存在,则进行更新
         CrmCustomerPoolConfigDO dbConfig = getCustomerPoolConfig();
-        CrmCustomerPoolConfigDO poolConfig = CrmCustomerConvert.INSTANCE.convert(saveReqVO);
+        CrmCustomerPoolConfigDO poolConfig = BeanUtils.toBean(saveReqVO, CrmCustomerPoolConfigDO.class);
         if (Objects.nonNull(dbConfig)) {
             customerPoolConfigMapper.updateById(poolConfig.setId(dbConfig.getId()));
             // 记录操作日志上下文

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

@@ -18,6 +18,9 @@ import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 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;
+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.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -26,6 +29,7 @@ import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.service.impl.DiffParseFunction;
 import com.mzt.logapi.starter.annotation.LogRecord;
 import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -59,6 +63,15 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     private CrmPermissionService permissionService;
     @Resource
     private CrmCustomerLimitConfigService customerLimitConfigService;
+    @Resource
+    @Lazy
+    private CrmContactService contactService;
+    @Resource
+    @Lazy
+    private CrmBusinessService businessService;
+    @Resource
+    @Lazy
+    private CrmContractService contractService;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -73,7 +86,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         validateCustomerExceedOwnerLimit(createReqVO.getOwnerUserId(), 1);
 
         // 2. 插入客户
-        CrmCustomerDO customer = CrmCustomerConvert.INSTANCE.convert(createReqVO)
+        CrmCustomerDO customer = BeanUtils.toBean(createReqVO, CrmCustomerDO.class)
                 .setLockStatus(false).setDealStatus(false)
                 .setContactLastTime(LocalDateTime.now());
         // TODO @puhui999:可能要加个 receiveTime 字段,记录最后接收时间
@@ -100,7 +113,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         CrmCustomerDO oldCustomer = validateCustomerExists(updateReqVO.getId());
 
         // 2. 更新客户
-        CrmCustomerDO updateObj = CrmCustomerConvert.INSTANCE.convert(updateReqVO);
+        CrmCustomerDO updateObj = BeanUtils.toBean(updateReqVO, CrmCustomerDO.class);
         customerMapper.updateById(updateObj);
 
         // 3. 记录操作日志上下文
@@ -116,8 +129,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     public void deleteCustomer(Long id) {
         // 校验存在
         CrmCustomerDO customer = validateCustomerExists(id);
-        // TODO @puhui999:如果有联系人、商机,则不允许删除;
-
+        // 检查引用
+        checkCustomerReference(id);
         // 删除
         customerMapper.deleteById(id);
         // 删除数据权限
@@ -128,6 +141,23 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         LogRecordContext.putVariable("customerName", customer.getName());
     }
 
+    /**
+     * 校验客户是否被引用
+     *
+     * @param id 客户编号
+     */
+    private void checkCustomerReference(Long id) {
+        if (contactService.getContactCountByCustomerId(id) > 0) {
+            throw exception(CUSTOMER_DELETE_FAIL_HAVE_REFERENCE, CrmBizTypeEnum.CRM_CONTACT.getName());
+        }
+        if (businessService.getBusinessCountByCustomerId(id) > 0) {
+            throw exception(CUSTOMER_DELETE_FAIL_HAVE_REFERENCE, CrmBizTypeEnum.CRM_BUSINESS.getName());
+        }
+        if (contractService.getContractCountByCustomerId(id) > 0) {
+            throw exception(CUSTOMER_DELETE_FAIL_HAVE_REFERENCE, CrmBizTypeEnum.CRM_CONTRACT.getName());
+        }
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_TRANSFER_SUB_TYPE, bizNo = "{{#reqVO.id}}",
@@ -145,10 +175,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         // 2.2 转移后重新设置负责人
         customerMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
 
-        // 3. TODO 记录转移日志
-        // 记录操作日志上下文
-        // TODO @puhui999:crmCustomer=》customer,也看看其他有没类似的情况哈
-        LogRecordContext.putVariable("crmCustomer", customer);
+        // 3. 记录转移日志
+        LogRecordContext.putVariable("customer", customer);
     }
 
     @Override
@@ -172,7 +200,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
         // 3. 记录操作日志上下文
         // tips: 因为这里使用的是老的状态所以记录时反着记录,也就是 lockStatus 为 true 那么就是解锁反之为锁定
-        LogRecordContext.putVariable("crmCustomer", customer);
+        LogRecordContext.putVariable("customer", customer);
     }
 
     // ==================== 公海相关操作 ====================
@@ -202,6 +230,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), customer.getId(),
                 CrmPermissionLevelEnum.OWNER.getLevel());
         // TODO @puhui999:联系人的负责人,也要设置为 null;这块和领取是对应的;因为领取后,负责人也要关联过来;
+        //      提问:那是不是可以这样理解客户所有联系人的负责人默认为客户的负责人,然后添加客户团队成员时才存在“同时分配给”的操作?
+        contactService.updateOwnerUserIdByCustomerId(customer.getId(), null);
 
         // 记录操作日志上下文
         LogRecordContext.putVariable("customerName", customer.getName());

+ 54 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordService.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.crm.service.followup;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordSaveReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
+import jakarta.validation.Valid;
+
+/**
+ * 跟进记录 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface CrmFollowUpRecordService {
+
+    /**
+     * 创建跟进记录
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createFollowUpRecord(@Valid CrmFollowUpRecordSaveReqVO createReqVO);
+
+    /**
+     * 更新跟进记录
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateFollowUpRecord(@Valid CrmFollowUpRecordSaveReqVO updateReqVO);
+
+    /**
+     * 删除跟进记录
+     *
+     * @param id 编号
+     */
+    void deleteFollowUpRecord(Long id);
+
+    /**
+     * 获得跟进记录
+     *
+     * @param id 编号
+     * @return 跟进记录
+     */
+    CrmFollowUpRecordDO getFollowUpRecord(Long id);
+
+    /**
+     * 获得跟进记录分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 跟进记录分页
+     */
+    PageResult<CrmFollowUpRecordDO> getFollowUpRecordPage(CrmFollowUpRecordPageReqVO pageReqVO);
+
+}

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

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.crm.service.followup;
+
+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.followup.vo.CrmFollowUpRecordPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordSaveReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.followup.CrmFollowUpRecordMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.FOLLOW_UP_RECORD_NOT_EXISTS;
+
+/**
+ * 跟进记录 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class CrmFollowUpRecordServiceImpl implements CrmFollowUpRecordService {
+
+    @Resource
+    private CrmFollowUpRecordMapper crmFollowUpRecordMapper;
+
+    @Override
+    public Long createFollowUpRecord(CrmFollowUpRecordSaveReqVO createReqVO) {
+        // 插入
+        CrmFollowUpRecordDO followUpRecord = BeanUtils.toBean(createReqVO, CrmFollowUpRecordDO.class);
+        crmFollowUpRecordMapper.insert(followUpRecord);
+        // 返回
+        return followUpRecord.getId();
+    }
+
+    @Override
+    public void updateFollowUpRecord(CrmFollowUpRecordSaveReqVO updateReqVO) {
+        // 校验存在
+        validateFollowUpRecordExists(updateReqVO.getId());
+        // 更新
+        CrmFollowUpRecordDO updateObj = BeanUtils.toBean(updateReqVO, CrmFollowUpRecordDO.class);
+        crmFollowUpRecordMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteFollowUpRecord(Long id) {
+        // 校验存在
+        validateFollowUpRecordExists(id);
+        // 删除
+        crmFollowUpRecordMapper.deleteById(id);
+    }
+
+    private void validateFollowUpRecordExists(Long id) {
+        if (crmFollowUpRecordMapper.selectById(id) == null) {
+            throw exception(FOLLOW_UP_RECORD_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmFollowUpRecordDO getFollowUpRecord(Long id) {
+        return crmFollowUpRecordMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<CrmFollowUpRecordDO> getFollowUpRecordPage(CrmFollowUpRecordPageReqVO pageReqVO) {
+        return crmFollowUpRecordMapper.selectPage(pageReqVO);
+    }
+
+}

+ 37 - 27
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java

@@ -20,6 +20,7 @@ import org.springframework.validation.annotation.Validated;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -36,7 +37,7 @@ import static cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnu
 public class CrmPermissionServiceImpl implements CrmPermissionService {
 
     @Resource
-    private CrmPermissionMapper crmPermissionMapper;
+    private CrmPermissionMapper permissionMapper;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -44,50 +45,59 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createPermission(CrmPermissionCreateReqBO createReqBO) {
-        // TODO @puhui999:排重
+        validatePermissionNotExists(Collections.singletonList(createReqBO));
         // 1. 校验用户是否存在
         adminUserApi.validateUserList(Collections.singletonList(createReqBO.getUserId()));
 
         // 2. 创建
         CrmPermissionDO permission = CrmPermissionConvert.INSTANCE.convert(createReqBO);
-        crmPermissionMapper.insert(permission);
+        permissionMapper.insert(permission);
         return permission.getId();
     }
 
     @Override
     public void createPermissionBatch(List<CrmPermissionCreateReqBO> createReqBOs) {
-        // TODO @puhui999:排重
+        validatePermissionNotExists(createReqBOs);
         // 1. 校验用户是否存在
         adminUserApi.validateUserList(convertSet(createReqBOs, CrmPermissionCreateReqBO::getUserId));
 
         // 2. 创建
         List<CrmPermissionDO> permissions = CrmPermissionConvert.INSTANCE.convertList(createReqBOs);
-        crmPermissionMapper.insertBatch(permissions);
+        permissionMapper.insertBatch(permissions);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updatePermission(CrmPermissionUpdateReqVO updateReqVO) {
-        // TODO @puhui999:排重
         // 1. 校验存在
-        validateCrmPermissionExists(updateReqVO.getIds());
+        validatePermissionExists(updateReqVO.getIds());
         // 2. 更新
         List<CrmPermissionDO> updateDO = CrmPermissionConvert.INSTANCE.convertList(updateReqVO);
-        crmPermissionMapper.updateBatch(updateDO);
+        permissionMapper.updateBatch(updateDO);
     }
 
-    private void validateCrmPermissionExists(Collection<Long> ids) {
-        List<CrmPermissionDO> permissionList = crmPermissionMapper.selectBatchIds(ids);
+    private void validatePermissionExists(Collection<Long> ids) {
+        List<CrmPermissionDO> permissionList = permissionMapper.selectBatchIds(ids);
         if (ObjUtil.notEqual(permissionList.size(), ids.size())) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
         }
     }
 
+    private void validatePermissionNotExists(Collection<CrmPermissionCreateReqBO> createReqBOs) {
+        Set<Integer> bizTypes = convertSet(createReqBOs, CrmPermissionCreateReqBO::getBizType);
+        Set<Long> bizIds = convertSet(createReqBOs, CrmPermissionCreateReqBO::getBizId);
+        Set<Long> userIds = convertSet(createReqBOs, CrmPermissionCreateReqBO::getUserId);
+        Long count = permissionMapper.selectListByBiz(bizTypes, bizIds, userIds);
+        if (count > 0) {
+            throw exception(CRM_PERMISSION_CREATE_FAIL);
+        }
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void transferPermission(CrmPermissionTransferReqBO transferReqBO) {
         // 1. 校验数据权限:是否是负责人,只有负责人才可以转移
-        CrmPermissionDO oldPermission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(
+        CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId(
                 transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId());
         String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType());
         if (oldPermission == null // 不是拥有者,并且不是超管
@@ -102,25 +112,25 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         adminUserApi.validateUserList(Collections.singletonList(transferReqBO.getNewOwnerUserId()));
 
         // 2. 修改新负责人的权限
-        List<CrmPermissionDO> permissions = crmPermissionMapper.selectByBizTypeAndBizId(
+        List<CrmPermissionDO> permissions = permissionMapper.selectByBizTypeAndBizId(
                 transferReqBO.getBizType(), transferReqBO.getBizId()); // 获得所有数据权限
         CrmPermissionDO permission = CollUtil.findOne(permissions,
                 item -> ObjUtil.equal(item.getUserId(), transferReqBO.getNewOwnerUserId()));
         if (permission == null) {
-            crmPermissionMapper.insert(new CrmPermissionDO().setBizType(transferReqBO.getBizType())
+            permissionMapper.insert(new CrmPermissionDO().setBizType(transferReqBO.getBizType())
                     .setBizId(transferReqBO.getBizId()).setUserId(transferReqBO.getNewOwnerUserId())
                     .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
         } else {
-            crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId())
+            permissionMapper.updateById(new CrmPermissionDO().setId(permission.getId())
                     .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
         }
 
         // 3. 修改老负责人的权限
         if (transferReqBO.getOldOwnerPermissionLevel() != null) {
-            crmPermissionMapper.updateById(new CrmPermissionDO().setId(oldPermission.getId())
+            permissionMapper.updateById(new CrmPermissionDO().setId(oldPermission.getId())
                     .setLevel(transferReqBO.getOldOwnerPermissionLevel()));
         } else {
-            crmPermissionMapper.deleteById(oldPermission.getId());
+            permissionMapper.deleteById(oldPermission.getId());
         }
     }
 
@@ -128,19 +138,19 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Transactional(rollbackFor = Exception.class)
     public void deletePermission(Integer bizType, Long bizId, Integer level) {
         // 校验存在
-        List<CrmPermissionDO> permissions = crmPermissionMapper.selectListByBizTypeAndBizIdAndLevel(
+        List<CrmPermissionDO> permissions = permissionMapper.selectListByBizTypeAndBizIdAndLevel(
                 bizType, bizId, level);
         if (CollUtil.isEmpty(permissions)) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
         }
 
         // 删除数据权限
-        crmPermissionMapper.deleteBatchIds(convertSet(permissions, CrmPermissionDO::getId));
+        permissionMapper.deleteBatchIds(convertSet(permissions, CrmPermissionDO::getId));
     }
 
     @Override
     public void deletePermission(Integer bizType, Long bizId) {
-        int deletedCount = crmPermissionMapper.deletePermission(bizType, bizId);
+        int deletedCount = permissionMapper.deletePermission(bizType, bizId);
         if (deletedCount == 0) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
         }
@@ -148,7 +158,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
 
     @Override
     public void deletePermissionBatch(Collection<Long> ids, Long userId) {
-        List<CrmPermissionDO> permissions = crmPermissionMapper.selectBatchIds(ids);
+        List<CrmPermissionDO> permissions = permissionMapper.selectBatchIds(ids);
         if (CollUtil.isEmpty(permissions)) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
         }
@@ -157,19 +167,19 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
             throw exception(CRM_PERMISSION_DELETE_FAIL);
         }
         // 校验操作人是否为负责人
-        CrmPermissionDO permission = crmPermissionMapper.selectByIdAndUserId(permissions.get(0).getBizId(), userId);
+        CrmPermissionDO permission = permissionMapper.selectByIdAndUserId(permissions.get(0).getBizId(), userId);
         if (!CrmPermissionLevelEnum.isOwner(permission.getLevel())) {
             throw exception(CRM_PERMISSION_DELETE_DENIED);
         }
 
         // 删除数据权限
-        crmPermissionMapper.deleteBatchIds(ids);
+        permissionMapper.deleteBatchIds(ids);
     }
 
     @Override
     public void deleteSelfPermission(Long id, Long userId) {
         // 校验数据存在且是自己
-        CrmPermissionDO permission = crmPermissionMapper.selectByIdAndUserId(id, userId);
+        CrmPermissionDO permission = permissionMapper.selectByIdAndUserId(id, userId);
         if (permission == null) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
         }
@@ -179,22 +189,22 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         }
 
         // 删除
-        crmPermissionMapper.deleteById(id);
+        permissionMapper.deleteById(id);
     }
 
     @Override
     public List<CrmPermissionDO> getPermissionListByBiz(Integer bizType, Long bizId) {
-        return crmPermissionMapper.selectByBizTypeAndBizId(bizType, bizId);
+        return permissionMapper.selectByBizTypeAndBizId(bizType, bizId);
     }
 
     @Override
     public List<CrmPermissionDO> getPermissionListByBiz(Integer bizType, Collection<Long> bizIds) {
-        return crmPermissionMapper.selectByBizTypeAndBizIds(bizType, bizIds);
+        return permissionMapper.selectByBizTypeAndBizIds(bizType, bizIds);
     }
 
     @Override
     public List<CrmPermissionDO> getPermissionListByBizTypeAndUserId(Integer bizType, Long userId) {
-        return crmPermissionMapper.selectListByBizTypeAndUserId(bizType, userId);
+        return permissionMapper.selectListByBizTypeAndUserId(bizType, userId);
     }
 
 }

+ 0 - 9
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.crm.service.receivable;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanTransferReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
@@ -78,12 +77,4 @@ public interface CrmReceivablePlanService {
      */
     PageResult<CrmReceivablePlanDO> getReceivablePlanPageByCustomerId(CrmReceivablePlanPageReqVO pageReqVO);
 
-    /**
-     * 回款计划转移
-     *
-     * @param reqVO  请求
-     * @param userId 用户编号
-     */
-    void transferReceivablePlan(CrmReceivablePlanTransferReqVO reqVO, Long userId);
-
 }

+ 30 - 24
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java

@@ -4,9 +4,9 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.util.ObjectUtil;
 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.receivable.vo.plan.CrmReceivablePlanCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanTransferReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivablePlanConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
@@ -20,6 +20,9 @@ import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import com.mzt.logapi.context.LogRecordContext;
+import com.mzt.logapi.service.impl.DiffParseFunction;
+import com.mzt.logapi.starter.annotation.LogRecord;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -29,6 +32,7 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
 
 // TODO @liuhongfeng:参考 CrmReceivableServiceImpl 写的 todo 哈;
 
@@ -49,10 +53,11 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
     @Resource
     private CrmCustomerService customerService;
     @Resource
-    private CrmPermissionService crmPermissionService;
+    private CrmPermissionService permissionService;
 
     @Override
-    // TODO @puhui999:操作日志
+    @LogRecord(type = CRM_RECEIVABLE_PLAN_TYPE, subType = CRM_RECEIVABLE_PLAN_CREATE_SUB_TYPE, bizNo = "{{#receivablePlan.id}}",
+            success = CRM_RECEIVABLE_PLAN_CREATE_SUCCESS)
     public Long createReceivablePlan(CrmReceivablePlanCreateReqVO createReqVO, Long userId) {
         // TODO @liuhongfeng:第几期的计算;基于是 contractId + contractDO 的第几个还款
         // TODO @liuhongfeng contractId:校验合同是否存在
@@ -64,9 +69,12 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
 
         receivablePlanMapper.insert(receivablePlan);
         // 创建数据权限
-        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(userId)
+        permissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(userId)
                 .setBizType(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType()).setBizId(receivablePlan.getId())
                 .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+
+        // 4. 记录操作日志上下文
+        LogRecordContext.putVariable("receivablePlan", receivablePlan);
         return receivablePlan.getId();
     }
 
@@ -89,31 +97,44 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
     }
 
     @Override
+    @LogRecord(type = CRM_RECEIVABLE_PLAN_TYPE, subType = CRM_RECEIVABLE_PLAN_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
+            success = CRM_RECEIVABLE_PLAN_UPDATE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE_PLAN, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
-    // TODO @puhui999:操作日志
     public void updateReceivablePlan(CrmReceivablePlanUpdateReqVO updateReqVO) {
         // TODO @liuhongfeng:如果已经有对应的还款,则不允许编辑;
         // 校验存在
-        validateReceivablePlanExists(updateReqVO.getId());
+        CrmReceivablePlanDO oldReceivablePlan = validateReceivablePlanExists(updateReqVO.getId());
 
         // 更新
         CrmReceivablePlanDO updateObj = CrmReceivablePlanConvert.INSTANCE.convert(updateReqVO);
         receivablePlanMapper.updateById(updateObj);
+
+        // 3. 记录操作日志上下文
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanUpdateReqVO.class));
+        LogRecordContext.putVariable("receivablePlan", oldReceivablePlan);
     }
 
     @Override
+    @LogRecord(type = CRM_RECEIVABLE_PLAN_TYPE, subType = CRM_RECEIVABLE_PLAN_DELETE_SUB_TYPE, bizNo = "{{#id}}",
+            success = CRM_RECEIVABLE_PLAN_DELETE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE_PLAN, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteReceivablePlan(Long id) {
         // 校验存在
-        validateReceivablePlanExists(id);
+        CrmReceivablePlanDO receivablePlan = validateReceivablePlanExists(id);
         // 删除
         receivablePlanMapper.deleteById(id);
+        // 删除数据权限
+        permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), id);
+        // 记录操作日志上下文
+        LogRecordContext.putVariable("receivablePlan", receivablePlan);
     }
 
-    private void validateReceivablePlanExists(Long id) {
-        if (receivablePlanMapper.selectById(id) == null) {
+    private CrmReceivablePlanDO validateReceivablePlanExists(Long id) {
+        CrmReceivablePlanDO receivablePlan = receivablePlanMapper.selectById(id);
+        if (receivablePlan == null) {
             throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
         }
+        return receivablePlan;
     }
 
     @Override
@@ -141,19 +162,4 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
         return receivablePlanMapper.selectPageByCustomerId(pageReqVO);
     }
 
-    // TODO @puhui999:这个没有 transfer 接口;可能是的哈
-    @Override
-    public void transferReceivablePlan(CrmReceivablePlanTransferReqVO reqVO, Long userId) {
-        // 1 校验回款计划是否存在
-        validateReceivablePlanExists(reqVO.getId());
-
-        // 2.1 数据权限转移
-        crmPermissionService.transferPermission(
-                CrmReceivablePlanConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType()));
-        // 2.2 设置新的负责人
-        receivablePlanMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
-
-        // 3. TODO 记录转移日志
-    }
-
 }

+ 2 - 10
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.crm.service.receivable;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableTransferReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
@@ -23,9 +22,10 @@ public interface CrmReceivableService {
      * 创建回款
      *
      * @param createReqVO 创建信息
+     * @param userId      用户编号
      * @return 编号
      */
-    Long createReceivable(@Valid CrmReceivableCreateReqVO createReqVO);
+    Long createReceivable(@Valid CrmReceivableCreateReqVO createReqVO, Long userId);
 
     /**
      * 更新回款
@@ -78,12 +78,4 @@ public interface CrmReceivableService {
      */
     PageResult<CrmReceivableDO> getReceivablePageByCustomerId(CrmReceivablePageReqVO pageReqVO);
 
-    /**
-     * 回款转移
-     *
-     * @param reqVO  请求
-     * @param userId 用户编号
-     */
-    void transferReceivable(CrmReceivableTransferReqVO reqVO, Long userId);
-
 }

+ 37 - 28
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java

@@ -5,9 +5,9 @@ import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 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.receivable.vo.receivable.CrmReceivableCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableTransferReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivableConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
@@ -22,6 +22,10 @@ import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPerm
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import com.mzt.logapi.context.LogRecordContext;
+import com.mzt.logapi.service.impl.DiffParseFunction;
+import com.mzt.logapi.starter.annotation.LogRecord;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -31,6 +35,7 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
 
 /**
  * CRM 回款 Service 实现类
@@ -51,11 +56,12 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     @Resource
     private CrmReceivablePlanService receivablePlanService;
     @Resource
-    private CrmPermissionService crmPermissionService;
+    private CrmPermissionService permissionService;
 
     @Override
-    // TODO @puhui999:操作日志
-    public Long createReceivable(CrmReceivableCreateReqVO createReqVO) {
+    @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_CREATE_SUB_TYPE, bizNo = "{{#receivable.id}}",
+            success = CRM_RECEIVABLE_CREATE_SUCCESS)
+    public Long createReceivable(CrmReceivableCreateReqVO createReqVO, Long userId) {
         // 插入还款
         CrmReceivableDO receivable = CrmReceivableConvert.INSTANCE.convert(createReqVO);
         if (ObjectUtil.isNull(receivable.getAuditStatus())) {
@@ -67,8 +73,12 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
         checkReceivable(receivable);
 
         receivableMapper.insert(receivable);
-
+        // 3. 创建数据权限
+        permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_RECEIVABLE.getType())
+                .setBizId(receivable.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
         // TODO @liuhongfeng:需要更新关联的 plan
+        // 记录操作日志上下文
+        LogRecordContext.putVariable("receivable", receivable);
         return receivable.getId();
     }
 
@@ -98,11 +108,12 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     }
 
     @Override
-    // TODO @puhui999:操作日志
-    // TODO @puhui999:权限校验
+    @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
+            success = CRM_RECEIVABLE_UPDATE_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
     public void updateReceivable(CrmReceivableUpdateReqVO updateReqVO) {
         // 校验存在
-        validateReceivableExists(updateReqVO.getId());
+        CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId());
         // TODO @liuhongfeng:只有在草稿、审核中,可以提交修改
 
         // 更新还款
@@ -110,6 +121,9 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
         receivableMapper.updateById(updateObj);
 
         // TODO @liuhongfeng:需要更新关联的 plan
+        // 3. 记录操作日志上下文
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivable, CrmReceivableUpdateReqVO.class));
+        LogRecordContext.putVariable("receivable", oldReceivable);
     }
 
     // TODO @liuhongfeng:缺一个取消合同的接口;只有草稿、审批中可以取消;CrmAuditStatusEnum
@@ -117,24 +131,33 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     // TODO @liuhongfeng:缺一个发起审批的接口;只有草稿可以发起审批;CrmAuditStatusEnum
 
     @Override
-    // TODO @puhui999:操作日志
-    // TODO @puhui999:权限校验
+    @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_DELETE_SUB_TYPE, bizNo = "{{#id}}",
+            success = CRM_RECEIVABLE_DELETE_SUCCESS)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteReceivable(Long id) {
         // TODO @liuhongfeng:如果被 CrmReceivablePlanDO 所使用,则不允许删除
         // 校验存在
-        validateReceivableExists(id);
+        CrmReceivableDO receivable = validateReceivableExists(id);
         // 删除
         receivableMapper.deleteById(id);
+
+        // 删除数据权限
+        permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), id);
+
+        // 记录操作日志上下文
+        LogRecordContext.putVariable("receivable", receivable);
     }
 
-    private void validateReceivableExists(Long id) {
-        if (receivableMapper.selectById(id) == null) {
+    private CrmReceivableDO validateReceivableExists(Long id) {
+        CrmReceivableDO receivable = receivableMapper.selectById(id);
+        if (receivable == null) {
             throw exception(RECEIVABLE_NOT_EXISTS);
         }
+        return receivable;
     }
 
-    // TODO @芋艿:数据权限
     @Override
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#id", level = CrmPermissionLevelEnum.READ)
     public CrmReceivableDO getReceivable(Long id) {
         return receivableMapper.selectById(id);
     }
@@ -158,18 +181,4 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
         return receivableMapper.selectPageByCustomerId(pageReqVO);
     }
 
-    @Override
-    public void transferReceivable(CrmReceivableTransferReqVO reqVO, Long userId) {
-        // 1 校验回款是否存在
-        validateReceivableExists(reqVO.getId());
-
-        // 2.1 数据权限转移
-        crmPermissionService.transferPermission(
-                CrmReceivableConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_RECEIVABLE.getType()));
-        // 2.2 设置新的负责人
-        receivableMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
-
-        // 3. TODO 记录转移日志
-    }
-
 }

+ 2 - 1
yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivableServiceImplTest.java

@@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test;
 import org.springframework.context.annotation.Import;
 
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
@@ -42,7 +43,7 @@ public class CrmCrmReceivableServiceImplTest extends BaseDbUnitTest {
         CrmReceivableCreateReqVO reqVO = randomPojo(CrmReceivableCreateReqVO.class);
 
         // 调用
-        Long receivableId = receivableService.createReceivable(reqVO);
+        Long receivableId = receivableService.createReceivable(reqVO, getLoginUserId());
         // 断言
         assertNotNull(receivableId);
         // 校验记录的属性是否正确

+ 17 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/operatelog/SysParseFunctionNameConstants.java

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.system.enums.operatelog;
+
+/**
+ * functionName 常量枚举
+ * 方便别的模块调用
+ *
+ * @author HUIHUI
+ */
+public interface SysParseFunctionNameConstants {
+
+    String GET_ADMIN_USER_BY_ID = "getAdminUserById"; // 获取用户信息
+    String GET_DEPT_BY_ID = "getDeptById"; // 获取部门信息
+    String GET_AREA = "getArea"; // 获取区域信息
+    String GET_SEX = "getSex"; // 获取性别
+    String GET_BOOLEAN = "getBoolean"; // 获取是否
+
+}

+ 3 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/AdminUserParseFunction.java

@@ -8,6 +8,8 @@ import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_ADMIN_USER_BY_ID;
+
 /**
  * 管理员名字的 {@link IParseFunction} 实现类
  *
@@ -22,7 +24,7 @@ public class AdminUserParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return "getAdminUserById";
+        return GET_ADMIN_USER_BY_ID;
     }
 
     @Override

+ 3 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/AreaParseFunction.java

@@ -6,6 +6,8 @@ import com.mzt.logapi.service.IParseFunction;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_AREA;
+
 /**
  * 地名的 {@link IParseFunction} 实现类
  *
@@ -22,7 +24,7 @@ public class AreaParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return "getAreaById";
+        return GET_AREA;
     }
 
     @Override

+ 6 - 6
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmBooleanParseFunction.java → yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/BooleanParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.operatelog.core;
+package cn.iocoder.yudao.module.system.framework.operatelog.core;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
@@ -7,16 +7,16 @@ import com.mzt.logapi.service.IParseFunction;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_BOOLEAN;
+
 /**
- * 行业的 {@link IParseFunction} 实现类
+ * 是否类型的 {@link IParseFunction} 实现类
  *
  * @author HUIHUI
  */
 @Component
 @Slf4j
-public class CrmBooleanParseFunction implements IParseFunction {
-
-    public static final String NAME = "getBooleanById";
+public class BooleanParseFunction implements IParseFunction {
 
     @Override
     public boolean executeBefore() {
@@ -25,7 +25,7 @@ public class CrmBooleanParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return NAME;
+        return GET_BOOLEAN;
     }
 
     @Override

+ 3 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/DeptParseFunction.java

@@ -8,6 +8,8 @@ import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_DEPT_BY_ID;
+
 /**
  * 管理员名字的 {@link IParseFunction} 实现类
  *
@@ -22,7 +24,7 @@ public class DeptParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return "getDeptById";
+        return GET_DEPT_BY_ID;
     }
 
     @Override

+ 5 - 5
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmSexParseFunction.java → yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/operatelog/core/SexParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.operatelog.core;
+package cn.iocoder.yudao.module.system.framework.operatelog.core;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
@@ -7,6 +7,8 @@ import com.mzt.logapi.service.IParseFunction;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import static cn.iocoder.yudao.module.system.enums.operatelog.SysParseFunctionNameConstants.GET_SEX;
+
 /**
  * 行业的 {@link IParseFunction} 实现类
  *
@@ -14,9 +16,7 @@ import org.springframework.stereotype.Component;
  */
 @Component
 @Slf4j
-public class CrmSexParseFunction implements IParseFunction {
-
-    public static final String NAME = "getSexById";
+public class SexParseFunction implements IParseFunction {
 
     @Override
     public boolean executeBefore() {
@@ -25,7 +25,7 @@ public class CrmSexParseFunction implements IParseFunction {
 
     @Override
     public String functionName() {
-        return NAME;
+        return GET_SEX;
     }
 
     @Override