From 599d13b1ac54968e4d26a3e8e811bbf6f67987aa Mon Sep 17 00:00:00 2001
From: youkehai <717407966@qq.com>
Date: Tue, 10 Oct 2023 11:36:24 +0800
Subject: [PATCH 001/101] =?UTF-8?q?feat:=20=E9=9A=8F=E4=BE=BF=E6=B5=8B?=
 =?UTF-8?q?=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/cn/iocoder/yudao/framework/common/package-info.java     | 1 +
 1 file changed, 1 insertion(+)

diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java
index f3f2574e5..09c43f748 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java
@@ -1,6 +1,7 @@
 /**
  * 基础的通用类,和框架无关
  *
+ * sdadsdsd
  * 例如说,CommonResult 为通用返回
  */
 package cn.iocoder.yudao.framework.common;

From 7e312aae027efd02b2e83d8429c13ac2a159520b Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 15 Oct 2023 20:16:46 +0800
Subject: [PATCH 002/101] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20crm=20?=
 =?UTF-8?q?=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pom.xml                                       |  1 +
 .../enums/ServiceErrorCodeRange.java          |  5 +-
 yudao-module-crm/pom.xml                      | 25 +++++++++
 yudao-module-crm/yudao-module-crm-api/pom.xml | 33 +++++++++++
 .../yudao/module/crm/api/package-info.java    |  4 ++
 .../module/crm/enums/ErrorCodeConstants.java  | 12 ++++
 yudao-module-crm/yudao-module-crm-biz/pom.xml | 55 +++++++++++++++++++
 .../yudao/module/crm/api/package-info.java    |  4 ++
 .../admin/business/package-info.java          |  4 ++
 .../controller/admin/clue/package-info.java   |  4 ++
 .../admin/contact/package-info.java           |  4 ++
 .../admin/contract/package-info.java          |  4 ++
 .../admin/customer/CrmCustomerController.http |  4 ++
 .../admin/customer/CrmCustomerController.java | 23 ++++++++
 .../admin/product/package-info.java           |  4 ++
 .../admin/receivable/package-info.java        |  4 ++
 .../crm/controller/app/package-info.java      |  4 ++
 .../module/crm/controller/package-info.java   |  6 ++
 .../module/crm/convert/package-info.java      |  6 ++
 ...�道 Spring Boot 对象转换 MapStruct 入门》.md |  1 +
 .../dal/dataobject/business/package-info.java |  4 ++
 .../crm/dal/dataobject/clue/package-info.java |  4 ++
 .../dal/dataobject/contact/package-info.java  |  4 ++
 .../dal/dataobject/contract/package-info.java |  4 ++
 .../dal/dataobject/customer/package-info.java |  4 ++
 .../dal/dataobject/product/package-info.java  |  4 ++
 .../dataobject/receivable/package-info.java   |  4 ++
 .../crm/dal/mysql/business/package-info.java  |  4 ++
 .../crm/dal/mysql/clue/package-info.java      |  4 ++
 .../crm/dal/mysql/contact/package-info.java   |  4 ++
 .../crm/dal/mysql/contract/package-info.java  |  4 ++
 .../crm/dal/mysql/customer/package-info.java  |  4 ++
 .../crm/dal/mysql/product/package-info.java   |  4 ++
 .../dal/mysql/receivable/package-info.java    |  4 ++
 .../module/crm/framework/package-info.java    |  6 ++
 .../yudao/module/crm/package-info.java        | 10 ++++
 .../crm/service/business/package-info.java    |  4 ++
 .../module/crm/service/clue/package-info.java |  4 ++
 .../crm/service/contact/package-info.java     |  4 ++
 .../crm/service/contract/package-info.java    |  4 ++
 .../crm/service/customer/package-info.java    |  4 ++
 .../crm/service/product/package-info.java     |  4 ++
 .../crm/service/receivable/package-info.java  |  4 ++
 yudao-server/pom.xml                          |  7 +++
 44 files changed, 313 insertions(+), 1 deletion(-)
 create mode 100644 yudao-module-crm/pom.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-api/pom.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/pom.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/app/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/product/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/package-info.java

diff --git a/pom.xml b/pom.xml
index f0d7064c7..45f82f2b4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
 <!--        <module>yudao-module-mp</module>-->
 <!--        <module>yudao-module-pay</module>-->
 <!--        <module>yudao-module-mall</module>-->
+        <module>yudao-module-crm</module>
         <!-- 示例项目 -->
         <module>yudao-example</module>
     </modules>
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java
index be22815eb..94dd67c9e 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java
@@ -35,9 +35,12 @@ public class ServiceErrorCodeRange {
     // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
     // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
     // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
-    // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
     // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
+
+    // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
     // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
     // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
 
+    // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
+
 }
diff --git a/yudao-module-crm/pom.xml b/yudao-module-crm/pom.xml
new file mode 100644
index 000000000..2a7b748c3
--- /dev/null
+++ b/yudao-module-crm/pom.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modules>
+        <module>yudao-module-crm-api</module>
+        <module>yudao-module-crm-biz</module>
+    </modules>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-crm</artifactId>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+
+    <description>
+        crm 包下,客户关系管理(Customer Relationship Management)。
+        例如说:客户、联系人、商机、合同、回款等等
+    </description>
+
+</project>
diff --git a/yudao-module-crm/yudao-module-crm-api/pom.xml b/yudao-module-crm/yudao-module-crm-api/pom.xml
new file mode 100644
index 000000000..94e129626
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-crm</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-crm-api</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        crm 模块 API,暴露给其它模块调用
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <!-- 参数校验 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java
new file mode 100644
index 000000000..c38bde7f5
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * crm API 包,定义暴露给其它模块的 API
+ */
+package cn.iocoder.yudao.module.crm.api;
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
new file mode 100644
index 000000000..b59eae2bd
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -0,0 +1,12 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+/**
+ * CRM 错误码枚举类
+ * <p>
+ * crm 系统,使用 1-020-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/pom.xml b/yudao-module-crm/yudao-module-crm-biz/pom.xml
new file mode 100644
index 000000000..dc2818da9
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/pom.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-crm</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-crm-biz</artifactId>
+
+    <name>${project.artifactId}</name>
+    <description>
+        crm 包下,客户关系管理(Customer Relationship Management)。
+        例如说:客户、联系人、商机、合同、回款等等
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-system-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java
new file mode 100644
index 000000000..5c4e2493e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * crm API 实现类,定义暴露给其它模块的 API
+ */
+package cn.iocoder.yudao.module.crm.api;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/package-info.java
new file mode 100644
index 000000000..07dec89b1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 商机(销售机会)
+ */
+package cn.iocoder.yudao.module.crm.controller.admin.business;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/package-info.java
new file mode 100644
index 000000000..0dc110844
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 线索
+ */
+package cn.iocoder.yudao.module.crm.controller.admin.clue;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/package-info.java
new file mode 100644
index 000000000..9b4afafef
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 联系人
+ */
+package cn.iocoder.yudao.module.crm.controller.admin.contact;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/package-info.java
new file mode 100644
index 000000000..b2f4b7b4a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 合同
+ */
+package cn.iocoder.yudao.module.crm.controller.admin.contract;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http
new file mode 100644
index 000000000..1dfa2c691
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http
@@ -0,0 +1,4 @@
+### 请求 /crm/customer/test 接口 => 成功
+GET {{baseUrl}}/crm/customer/test
+tenant-id: 1
+Authorization: Bearer {{token}}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
new file mode 100644
index 000000000..b5aee837e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 客户信息")
+@RestController
+@RequestMapping("/crm/customer")
+@Validated
+public class CrmCustomerController {
+
+    @GetMapping("/test")
+    public CommonResult<String> test() {
+        return success("hello");
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/package-info.java
new file mode 100644
index 000000000..6500da0b8
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 产品表
+ */
+package cn.iocoder.yudao.module.crm.controller.admin.product;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/package-info.java
new file mode 100644
index 000000000..199398287
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 回款
+ */
+package cn.iocoder.yudao.module.crm.controller.admin.receivable;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/app/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/app/package-info.java
new file mode 100644
index 000000000..78d85635c
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/app/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 占位
+ */
+package cn.iocoder.yudao.module.crm.controller.app;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/package-info.java
new file mode 100644
index 000000000..8354b3176
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 提供 RESTful API 给前端:
+ * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目
+ * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分
+ */
+package cn.iocoder.yudao.module.crm.controller;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/package-info.java
new file mode 100644
index 000000000..6fbc52508
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 提供 POJO 类的实体转换
+ *
+ * 目前使用 MapStruct 框架
+ */
+package cn.iocoder.yudao.module.crm.convert;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md
new file mode 100644
index 000000000..8153487b7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md	
@@ -0,0 +1 @@
+<http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/package-info.java
new file mode 100644
index 000000000..df6e44536
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 商机(销售机会)
+ */
+package cn.iocoder.yudao.module.crm.dal.dataobject.business;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/package-info.java
new file mode 100644
index 000000000..929b9b6fe
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 线索
+ */
+package cn.iocoder.yudao.module.crm.dal.dataobject.clue;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/package-info.java
new file mode 100644
index 000000000..dfe0898e3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 联系人
+ */
+package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/package-info.java
new file mode 100644
index 000000000..a981b5dfc
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 合同
+ */
+package cn.iocoder.yudao.module.crm.dal.dataobject.contract;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/package-info.java
new file mode 100644
index 000000000..78e98bcd9
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 客户
+ */
+package cn.iocoder.yudao.module.crm.dal.dataobject.customer;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/package-info.java
new file mode 100644
index 000000000..4c7282d73
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 产品表
+ */
+package cn.iocoder.yudao.module.crm.dal.dataobject.product;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/package-info.java
new file mode 100644
index 000000000..f27442cdf
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 回款
+ */
+package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/package-info.java
new file mode 100644
index 000000000..72863e1f4
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 商机(销售机会)
+ */
+package cn.iocoder.yudao.module.crm.dal.mysql.business;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/package-info.java
new file mode 100644
index 000000000..f9978e868
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 线索
+ */
+package cn.iocoder.yudao.module.crm.dal.mysql.clue;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/package-info.java
new file mode 100644
index 000000000..6cb7d4be2
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 联系人
+ */
+package cn.iocoder.yudao.module.crm.dal.mysql.contact;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/package-info.java
new file mode 100644
index 000000000..fb8045878
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 合同
+ */
+package cn.iocoder.yudao.module.crm.dal.mysql.contract;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/package-info.java
new file mode 100644
index 000000000..4173c0180
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 客户
+ */
+package cn.iocoder.yudao.module.crm.dal.mysql.customer;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/product/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/product/package-info.java
new file mode 100644
index 000000000..be0fa00a9
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/product/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 产品表
+ */
+package cn.iocoder.yudao.module.crm.dal.mysql.product;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/package-info.java
new file mode 100644
index 000000000..65b30aa92
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 回款
+ */
+package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/package-info.java
new file mode 100644
index 000000000..281e36c45
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 属于 crm 模块的 framework 封装
+ *
+ * @author 芋道源码
+ */
+package cn.iocoder.yudao.module.crm.framework;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/package-info.java
new file mode 100644
index 000000000..2ea5f9f41
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * crm 包下,客户关系管理(Customer Relationship Management)。
+ * 例如说:客户、联系人、商机、合同、回款等等
+ *
+ * 1. Controller URL:以 /crm/ 开头,避免和其它 Module 冲突
+ * 2. DataObject 表名:以 crm_ 开头,方便在数据库中区分
+ *
+ * 注意,由于 Crm 模块下,容易和其它模块重名,所以类名都加载 Crm 的前缀~
+ */
+package cn.iocoder.yudao.module.crm;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/package-info.java
new file mode 100644
index 000000000..8995e1242
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 商机(销售机会)
+ */
+package cn.iocoder.yudao.module.crm.service.business;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/package-info.java
new file mode 100644
index 000000000..5cb8b6ec7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 线索
+ */
+package cn.iocoder.yudao.module.crm.service.clue;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/package-info.java
new file mode 100644
index 000000000..e72077dd7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 联系人
+ */
+package cn.iocoder.yudao.module.crm.service.contact;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/package-info.java
new file mode 100644
index 000000000..743f159b7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 合同
+ */
+package cn.iocoder.yudao.module.crm.service.contract;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/package-info.java
new file mode 100644
index 000000000..1ae12a3df
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 客户
+ */
+package cn.iocoder.yudao.module.crm.service.customer;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/package-info.java
new file mode 100644
index 000000000..cae179aea
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 产品表
+ */
+package cn.iocoder.yudao.module.crm.service.product;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/package-info.java
new file mode 100644
index 000000000..4004b301d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 回款
+ */
+package cn.iocoder.yudao.module.crm.service.receivable;
diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index 5f0d12f33..2b7742246 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -91,6 +91,13 @@
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
 
+        <!-- CRM 相关模块。默认注释,保证编译速度 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-crm-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
         <!-- spring boot 配置所需依赖 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

From e26bd20a5b121f729cc42e5aa6168806a73f394f Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Mon, 16 Oct 2023 23:45:18 +0800
Subject: [PATCH 003/101] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E6=B5=8B?=
 =?UTF-8?q?=E8=AF=95=E9=85=8D=E7=BD=AE=E5=8F=8Aexcel=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 yudao-module-crm/yudao-module-crm-biz/pom.xml | 15 +++++-
 .../test/resources/application-unit-test.yaml | 50 +++++++++++++++++++
 .../src/test/resources/logback.xml            |  4 ++
 3 files changed, 67 insertions(+), 2 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/resources/application-unit-test.yaml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/resources/logback.xml

diff --git a/yudao-module-crm/yudao-module-crm-biz/pom.xml b/yudao-module-crm/yudao-module-crm-biz/pom.xml
index dc2818da9..5bedf5eac 100644
--- a/yudao-module-crm/yudao-module-crm-biz/pom.xml
+++ b/yudao-module-crm/yudao-module-crm-biz/pom.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
         <groupId>cn.iocoder.boot</groupId>
@@ -22,6 +22,11 @@
             <artifactId>yudao-module-system-api</artifactId>
             <version>${revision}</version>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-crm-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
         <!-- 业务组件 -->
         <dependency>
@@ -46,6 +51,12 @@
             <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
         </dependency>
 
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
+
         <!-- Test 测试相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/application-unit-test.yaml b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/application-unit-test.yaml
new file mode 100644
index 000000000..767f2526f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/application-unit-test.yaml
@@ -0,0 +1,50 @@
+spring:
+    main:
+        lazy-initialization: true # 开启懒加载,加快速度
+        banner-mode: off # 单元测试,禁用 Banner
+
+--- #################### 数据库相关配置 ####################
+
+spring:
+    # 数据源配置项
+    datasource:
+        name: ruoyi-vue-pro
+        url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+        driver-class-name: org.h2.Driver
+        username: sa
+        password:
+        druid:
+            async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
+            initial-size: 1 # 单元测试,配置为 1,提升启动速度
+    sql:
+        init:
+            schema-locations: classpath:/sql/create_tables.sql
+
+    # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
+    redis:
+        host: 127.0.0.1 # 地址
+        port: 16379 # 端口(单元测试,使用 16379 端口)
+        database: 0 # 数据库索引
+
+mybatis-plus:
+    lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
+    type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject
+
+--- #################### 定时任务相关配置 ####################
+
+--- #################### 配置中心相关配置 ####################
+
+--- #################### 服务保障相关配置 ####################
+
+# Lock4j 配置项(单元测试,禁用 Lock4j)
+
+# Resilience4j 配置项
+
+--- #################### 监控相关配置 ####################
+
+--- #################### 芋道相关配置 ####################
+
+# 芋道配置项,设置当前项目所有自定义的配置
+yudao:
+  info:
+    base-package: cn.iocoder.yudao
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/logback.xml b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/logback.xml
new file mode 100644
index 000000000..1d071e479
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/logback.xml
@@ -0,0 +1,4 @@
+<configuration>
+    <!-- 引用 Spring Boot 的 logback 基础配置 -->
+    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
+</configuration>

From eedb528412f5c790250d55663d88689821c52d3a Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Mon, 16 Oct 2023 23:53:17 +0800
Subject: [PATCH 004/101] =?UTF-8?q?feat:=20CRM/=E5=90=88=E5=90=8C=E7=AE=A1?=
 =?UTF-8?q?=E7=90=86=20-=20crud?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |   5 +-
 .../admin/contact/package-info.java           |   4 -
 .../admin/contract/ContractController.java    |  99 +++++++++
 .../admin/contract/vo/ContractBaseVO.java     |  72 +++++++
 .../contract/vo/ContractCreateReqVO.java      |  20 ++
 .../admin/contract/vo/ContractExcelVO.java    |  70 +++++++
 .../contract/vo/ContractExportReqVO.java      |  37 ++++
 .../admin/contract/vo/ContractPageReqVO.java  |  42 ++++
 .../admin/contract/vo/ContractRespVO.java     |  22 ++
 .../contract/vo/ContractUpdateReqVO.java      |  26 +++
 .../crm/convert/contract/ContractConvert.java |  36 ++++
 .../dal/dataobject/contract/ContractDO.java   | 104 ++++++++++
 .../dal/mysql/contract/ContractMapper.java    |  45 ++++
 .../crm/service/contract/ContractService.java |  75 +++++++
 .../service/contract/ContractServiceImpl.java |  90 ++++++++
 .../contract/ContractServiceImplTest.java     | 195 ++++++++++++++++++
 16 files changed, 937 insertions(+), 5 deletions(-)
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/ContractMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index b59eae2bd..532533d6c 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.crm.enums;
 
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
 /**
  * CRM 错误码枚举类
  * <p>
@@ -7,6 +9,7 @@ package cn.iocoder.yudao.module.crm.enums;
  */
 public interface ErrorCodeConstants {
 
-
+    // ========== 合同管理 1-020-000-000 ==========
+    ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/package-info.java
deleted file mode 100644
index 9b4afafef..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * 联系人
- */
-package cn.iocoder.yudao.module.crm.controller.admin.contact;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
new file mode 100644
index 000000000..3bc52c038
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.contract.vo.*;
+import cn.iocoder.yudao.module.crm.convert.contract.ContractConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import cn.iocoder.yudao.module.crm.service.contract.ContractService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+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/contract")
+@Validated
+public class ContractController {
+
+    @Resource
+    private ContractService contractService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建合同")
+    @PreAuthorize("@ss.hasPermission('crm:contract:create')")
+    public CommonResult<Long> createContract(@Valid @RequestBody ContractCreateReqVO createReqVO) {
+        return success(contractService.createContract(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新合同")
+    @PreAuthorize("@ss.hasPermission('crm:contract:update')")
+    public CommonResult<Boolean> updateContract(@Valid @RequestBody ContractUpdateReqVO updateReqVO) {
+        contractService.updateContract(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除合同")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:contract:delete')")
+    public CommonResult<Boolean> deleteContract(@RequestParam("id") Long id) {
+        contractService.deleteContract(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得合同")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
+    public CommonResult<ContractRespVO> getContract(@RequestParam("id") Long id) {
+        ContractDO contract = contractService.getContract(id);
+        return success(ContractConvert.INSTANCE.convert(contract));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得合同列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
+    public CommonResult<List<ContractRespVO>> getContractList(@RequestParam("ids") Collection<Long> ids) {
+        List<ContractDO> list = contractService.getContractList(ids);
+        return success(ContractConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得合同分页")
+    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
+    public CommonResult<PageResult<ContractRespVO>> getContractPage(@Valid ContractPageReqVO pageVO) {
+        PageResult<ContractDO> pageResult = contractService.getContractPage(pageVO);
+        return success(ContractConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出合同 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:contract:export')")
+    @OperateLog(type = EXPORT)
+    public void exportContractExcel(@Valid ContractExportReqVO exportReqVO,
+                                    HttpServletResponse response) throws IOException {
+        List<ContractDO> list = contractService.getContractList(exportReqVO);
+        // 导出 Excel
+        List<ContractExcelVO> datas = ContractConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "合同.xls", "数据", ContractExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java
new file mode 100644
index 000000000..ad43604a1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 合同 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ContractBaseVO {
+
+    @Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
+    @NotNull(message = "合同名称不能为空")
+    private String name;
+
+    @Schema(description = "客户编号", example = "18336")
+    private Long customerId;
+
+    @Schema(description = "商机编号", example = "10864")
+    private Long businessId;
+
+    @Schema(description = "工作流编号", example = "1043")
+    private Long processInstanceId;
+
+    @Schema(description = "下单日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime orderDate;
+
+    @Schema(description = "负责人的用户编号", example = "17144")
+    private Long ownerUserId;
+
+    @Schema(description = "合同编号")
+    private String no;
+
+    @Schema(description = "开始时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime startTime;
+
+    @Schema(description = "结束时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime endTime;
+
+    @Schema(description = "合同金额", example = "5617")
+    private Integer price;
+
+    @Schema(description = "整单折扣")
+    private Integer discountPercent;
+
+    @Schema(description = "产品总金额", example = "19510")
+    private Integer productPrice;
+
+    @Schema(description = "联系人编号", example = "18546")
+    private Long contactId;
+
+    @Schema(description = "公司签约人", example = "14036")
+    private Long signUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
new file mode 100644
index 000000000..38c95d23f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 合同创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContractCreateReqVO extends ContractBaseVO {
+
+    @Schema(description = "只读权限的用户编号数组")
+    private String roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private String rwUserIds;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java
new file mode 100644
index 000000000..895ac821a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 合同 Excel VO
+ *
+ * @author dhb52
+ */
+@Data
+public class ContractExcelVO {
+
+    @ExcelProperty("合同编号")
+    private Long id;
+
+    @ExcelProperty("合同名称")
+    private String name;
+
+    @ExcelProperty("客户编号")
+    private Long customerId;
+
+    @ExcelProperty("商机编号")
+    private Long businessId;
+
+    @ExcelProperty("工作流编号")
+    private Long processInstanceId;
+
+    @ExcelProperty("下单日期")
+    private LocalDateTime orderDate;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("合同编号")
+    private String no;
+
+    @ExcelProperty("开始时间")
+    private LocalDateTime startTime;
+
+    @ExcelProperty("结束时间")
+    private LocalDateTime endTime;
+
+    @ExcelProperty("合同金额")
+    private Integer price;
+
+    @ExcelProperty("整单折扣")
+    private Integer discountPercent;
+
+    @ExcelProperty("产品总金额")
+    private Integer productPrice;
+
+    @ExcelProperty("联系人编号")
+    private Long contactId;
+
+    @ExcelProperty("公司签约人")
+    private Long signUserId;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java
new file mode 100644
index 000000000..8de3b4c1b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+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 = "管理后台 - 合同 Excel 导出 Request VO,参数和 ContractPageReqVO 是一致的")
+@Data
+public class ContractExportReqVO {
+
+    @Schema(description = "合同名称", example = "王五")
+    private String name;
+
+    @Schema(description = "客户编号", example = "18336")
+    private Long customerId;
+
+    @Schema(description = "商机编号", example = "10864")
+    private Long businessId;
+
+    @Schema(description = "下单日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] orderDate;
+
+    @Schema(description = "合同编号")
+    private String no;
+
+    @Schema(description = "整单折扣")
+    private Integer discountPercent;
+
+    @Schema(description = "产品总金额", example = "19510")
+    private Integer productPrice;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java
new file mode 100644
index 000000000..01562be1b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.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 ContractPageReqVO extends PageParam {
+
+    @Schema(description = "合同名称", example = "王五")
+    private String name;
+
+    @Schema(description = "客户编号", example = "18336")
+    private Long customerId;
+
+    @Schema(description = "商机编号", example = "10864")
+    private Long businessId;
+
+    @Schema(description = "下单日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] orderDate;
+
+    @Schema(description = "合同编号")
+    private String no;
+
+    @Schema(description = "整单折扣")
+    private Integer discountPercent;
+
+    @Schema(description = "产品总金额", example = "19510")
+    private Integer productPrice;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java
new file mode 100644
index 000000000..a0ab1d248
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 合同 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContractRespVO extends ContractBaseVO {
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
new file mode 100644
index 000000000..f38ac7677
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 合同更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContractUpdateReqVO extends ContractBaseVO {
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "合同编号不能为空")
+    private Long id;
+
+    @Schema(description = "只读权限的用户编号数组")
+    private String roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private String rwUserIds;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
new file mode 100644
index 000000000..906dd7562
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.crm.convert.contract;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractExcelVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 合同 Convert
+ *
+ * @author dhb52
+ */
+@Mapper
+public interface ContractConvert {
+
+    ContractConvert INSTANCE = Mappers.getMapper(ContractConvert.class);
+
+    ContractDO convert(ContractCreateReqVO bean);
+
+    ContractDO convert(ContractUpdateReqVO bean);
+
+    ContractRespVO convert(ContractDO bean);
+
+    List<ContractRespVO> convertList(List<ContractDO> list);
+
+    PageResult<ContractRespVO> convertPage(PageResult<ContractDO> page);
+
+    List<ContractExcelVO> convertList02(List<ContractDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
new file mode 100644
index 000000000..8caa4d2cb
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.contract;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 合同 DO
+ *
+ * @author dhb52
+ */
+@TableName("crm_contract")
+@KeySequence("crm_contract_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ContractDO extends BaseDO {
+
+    /**
+     * 合同编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 合同名称
+     */
+    private String name;
+    /**
+     * 客户编号
+     */
+    private Long customerId;
+    /**
+     * 商机编号
+     */
+    private Long businessId;
+    /**
+     * 工作流编号
+     */
+    private Long processInstanceId;
+    /**
+     * 下单日期
+     */
+    private LocalDateTime orderDate;
+    /**
+     * 负责人的用户编号
+     */
+    private Long ownerUserId;
+    /**
+     * 合同编号
+     */
+    private String no;
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+    /**
+     * 结束时间
+     */
+    private LocalDateTime endTime;
+    /**
+     * 合同金额
+     */
+    private Integer price;
+    /**
+     * 整单折扣
+     */
+    private Integer discountPercent;
+    /**
+     * 产品总金额
+     */
+    private Integer productPrice;
+    /**
+     * 只读权限的用户编号数组
+     */
+    private String roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    private String rwUserIds;
+    /**
+     * 联系人编号
+     */
+    private Long contactId;
+    /**
+     * 公司签约人
+     */
+    private Long signUserId;
+    /**
+     * 最后跟进时间
+     */
+    private LocalDateTime contactLastTime;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/ContractMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/ContractMapper.java
new file mode 100644
index 000000000..47337518b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/ContractMapper.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.contract;
+
+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.contract.vo.ContractExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 合同 Mapper
+ *
+ * @author dhb52
+ */
+@Mapper
+public interface ContractMapper extends BaseMapperX<ContractDO> {
+
+    default PageResult<ContractDO> selectPage(ContractPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ContractDO>()
+            .likeIfPresent(ContractDO::getName, reqVO.getName())
+            .eqIfPresent(ContractDO::getCustomerId, reqVO.getCustomerId())
+            .eqIfPresent(ContractDO::getBusinessId, reqVO.getBusinessId())
+            .betweenIfPresent(ContractDO::getOrderDate, reqVO.getOrderDate())
+            .eqIfPresent(ContractDO::getNo, reqVO.getNo())
+            .eqIfPresent(ContractDO::getDiscountPercent, reqVO.getDiscountPercent())
+            .eqIfPresent(ContractDO::getProductPrice, reqVO.getProductPrice())
+            .orderByDesc(ContractDO::getId));
+    }
+
+    default List<ContractDO> selectList(ContractExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<ContractDO>()
+            .likeIfPresent(ContractDO::getName, reqVO.getName())
+            .eqIfPresent(ContractDO::getCustomerId, reqVO.getCustomerId())
+            .eqIfPresent(ContractDO::getBusinessId, reqVO.getBusinessId())
+            .betweenIfPresent(ContractDO::getOrderDate, reqVO.getOrderDate())
+            .eqIfPresent(ContractDO::getNo, reqVO.getNo())
+            .eqIfPresent(ContractDO::getDiscountPercent, reqVO.getDiscountPercent())
+            .eqIfPresent(ContractDO::getProductPrice, reqVO.getProductPrice())
+            .orderByDesc(ContractDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
new file mode 100644
index 000000000..6d6c9fe9f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.crm.service.contract;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 合同 Service 接口
+ *
+ * @author dhb52
+ */
+public interface ContractService {
+
+    /**
+     * 创建合同
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createContract(@Valid ContractCreateReqVO createReqVO);
+
+    /**
+     * 更新合同
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateContract(@Valid ContractUpdateReqVO updateReqVO);
+
+    /**
+     * 删除合同
+     *
+     * @param id 编号
+     */
+    void deleteContract(Long id);
+
+    /**
+     * 获得合同
+     *
+     * @param id 编号
+     * @return 合同
+     */
+    ContractDO getContract(Long id);
+
+    /**
+     * 获得合同列表
+     *
+     * @param ids 编号
+     * @return 合同列表
+     */
+    List<ContractDO> getContractList(Collection<Long> ids);
+
+    /**
+     * 获得合同分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 合同分页
+     */
+    PageResult<ContractDO> getContractPage(ContractPageReqVO pageReqVO);
+
+    /**
+     * 获得合同列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 合同列表
+     */
+    List<ContractDO> getContractList(ContractExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
new file mode 100644
index 000000000..e840626b9
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -0,0 +1,90 @@
+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.module.crm.controller.admin.contract.vo.ContractCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.contract.ContractConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.contract.ContractMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+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;
+
+/**
+ * 合同 Service 实现类
+ *
+ * @author dhb52
+ */
+@Service
+@Validated
+public class ContractServiceImpl implements ContractService {
+
+    @Resource
+    private ContractMapper contractMapper;
+
+    @Override
+    public Long createContract(ContractCreateReqVO createReqVO) {
+        // 插入
+        ContractDO contract = ContractConvert.INSTANCE.convert(createReqVO);
+        contractMapper.insert(contract);
+        // 返回
+        return contract.getId();
+    }
+
+    @Override
+    public void updateContract(ContractUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateContractExists(updateReqVO.getId());
+        // 更新
+        ContractDO updateObj = ContractConvert.INSTANCE.convert(updateReqVO);
+        contractMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteContract(Long id) {
+        // 校验存在
+        validateContractExists(id);
+        // 删除
+        contractMapper.deleteById(id);
+    }
+
+    private void validateContractExists(Long id) {
+        if (contractMapper.selectById(id) == null) {
+            throw exception(CONTRACT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ContractDO getContract(Long id) {
+        return contractMapper.selectById(id);
+    }
+
+    @Override
+    public List<ContractDO> getContractList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return contractMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<ContractDO> getContractPage(ContractPageReqVO pageReqVO) {
+        return contractMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<ContractDO> getContractList(ContractExportReqVO exportReqVO) {
+        return contractMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java
new file mode 100644
index 000000000..f9cbe3a38
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java
@@ -0,0 +1,195 @@
+package cn.iocoder.yudao.module.crm.service.contract;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.contract.ContractMapper;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTRACT_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link ContractServiceImpl} 的单元测试类
+ *
+ * @author dhb52
+ */
+@Import(ContractServiceImpl.class)
+public class ContractServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private ContractServiceImpl contractService;
+
+    @Resource
+    private ContractMapper contractMapper;
+
+    @Test
+    public void testCreateContract_success() {
+        // 准备参数
+        ContractCreateReqVO reqVO = randomPojo(ContractCreateReqVO.class);
+
+        // 调用
+        Long contractId = contractService.createContract(reqVO);
+        // 断言
+        assertNotNull(contractId);
+        // 校验记录的属性是否正确
+        ContractDO contract = contractMapper.selectById(contractId);
+        assertPojoEquals(reqVO, contract);
+    }
+
+    @Test
+    public void testUpdateContract_success() {
+        // mock 数据
+        ContractDO dbContract = randomPojo(ContractDO.class);
+        contractMapper.insert(dbContract);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        ContractUpdateReqVO reqVO = randomPojo(ContractUpdateReqVO.class, o -> {
+            o.setId(dbContract.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        contractService.updateContract(reqVO);
+        // 校验是否更新正确
+        ContractDO contract = contractMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, contract);
+    }
+
+    @Test
+    public void testUpdateContract_notExists() {
+        // 准备参数
+        ContractUpdateReqVO reqVO = randomPojo(ContractUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> contractService.updateContract(reqVO), CONTRACT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteContract_success() {
+        // mock 数据
+        ContractDO dbContract = randomPojo(ContractDO.class);
+        contractMapper.insert(dbContract);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbContract.getId();
+
+        // 调用
+        contractService.deleteContract(id);
+        // 校验数据不存在了
+        assertNull(contractMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteContract_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> contractService.deleteContract(id), CONTRACT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetContractPage() {
+        // mock 数据
+        ContractDO dbContract = randomPojo(ContractDO.class, o -> { // 等会查询到
+            o.setName(null);
+            o.setCustomerId(null);
+            o.setBusinessId(null);
+            o.setOrderDate(null);
+            o.setNo(null);
+            o.setDiscountPercent(null);
+            o.setProductPrice(null);
+        });
+        contractMapper.insert(dbContract);
+        // 测试 name 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setName(null)));
+        // 测试 customerId 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setCustomerId(null)));
+        // 测试 businessId 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setBusinessId(null)));
+        // 测试 orderDate 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setOrderDate(null)));
+        // 测试 no 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setNo(null)));
+        // 测试 discountPercent 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setDiscountPercent(null)));
+        // 测试 productPrice 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setProductPrice(null)));
+        // 准备参数
+        ContractPageReqVO reqVO = new ContractPageReqVO();
+        reqVO.setName(null);
+        reqVO.setCustomerId(null);
+        reqVO.setBusinessId(null);
+        reqVO.setOrderDate(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+        reqVO.setNo(null);
+        reqVO.setDiscountPercent(null);
+        reqVO.setProductPrice(null);
+
+        // 调用
+        PageResult<ContractDO> pageResult = contractService.getContractPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbContract, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetContractList() {
+        // mock 数据
+        ContractDO dbContract = randomPojo(ContractDO.class, o -> { // 等会查询到
+            o.setName("合同名称");
+            o.setCustomerId(null);
+            o.setBusinessId(null);
+            o.setOrderDate(null);
+            o.setNo(null);
+            o.setDiscountPercent(null);
+            o.setProductPrice(null);
+        });
+        contractMapper.insert(dbContract);
+        // 测试 name 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setName(null)));
+        // 测试 customerId 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setCustomerId(null)));
+        // 测试 businessId 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setBusinessId(null)));
+        // 测试 orderDate 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setOrderDate(null)));
+        // 测试 no 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setNo(null)));
+        // 测试 discountPercent 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setDiscountPercent(null)));
+        // 测试 productPrice 不匹配
+        contractMapper.insert(cloneIgnoreId(dbContract, o -> o.setProductPrice(null)));
+        // 准备参数
+        ContractExportReqVO reqVO = new ContractExportReqVO();
+        reqVO.setName(null);
+        reqVO.setCustomerId(null);
+        reqVO.setBusinessId(null);
+        reqVO.setOrderDate(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+        reqVO.setNo(null);
+        reqVO.setDiscountPercent(null);
+        reqVO.setProductPrice(null);
+
+        // 调用
+        List<ContractDO> list = contractService.getContractList(reqVO);
+        // 断言
+        assertEquals(1, list.size());
+        assertPojoEquals(dbContract, list.get(0));
+    }
+
+}

From 164be5732b4c022b61d42403019c899f8ecb2fbe Mon Sep 17 00:00:00 2001
From: dhb52 <dhb52@126.com>
Date: Mon, 16 Oct 2023 23:53:27 +0800
Subject: [PATCH 005/101] =?UTF-8?q?feat:=20CRM/=E5=90=88=E5=90=8C=E7=AE=A1?=
 =?UTF-8?q?=E7=90=86=20-=20SQL?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             | 39 ++++++++++++
 sql/mysql/crm_menu.sql                        | 63 +++++++++++++++++++
 .../src/test/resources/sql/clean.sql          |  1 +
 .../src/test/resources/sql/create_tables.sql  | 27 ++++++++
 4 files changed, 130 insertions(+)
 create mode 100644 sql/mysql/crm.sql
 create mode 100644 sql/mysql/crm_menu.sql
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
new file mode 100644
index 000000000..eeb2d12ac
--- /dev/null
+++ b/sql/mysql/crm.sql
@@ -0,0 +1,39 @@
+SET NAMES utf8mb4;
+
+-- ----------------------------
+-- 合同表
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_contract`;
+CREATE TABLE `crm_contract`
+(
+    `id`                  bigint                                                        NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
+    `name`                varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合同名称',
+    `customer_id`         bigint                                                                 DEFAULT NULL COMMENT '客户编号',
+    `business_id`         bigint                                                                 DEFAULT NULL COMMENT '商机编号',
+    `process_instance_id` bigint                                                                 DEFAULT NULL COMMENT '工作流编号',
+    `order_date`          datetime                                                               DEFAULT NULL COMMENT '下单日期',
+    `owner_user_id`       bigint                                                                 DEFAULT NULL COMMENT '负责人的用户编号',
+    `no`                  varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci          DEFAULT NULL COMMENT '合同编号',
+    `start_time`          datetime                                                               DEFAULT NULL COMMENT '开始时间',
+    `end_time`            datetime                                                               DEFAULT NULL COMMENT '结束时间',
+    `price`               int                                                                    DEFAULT NULL COMMENT '合同金额',
+    `discount_percent`    int                                                                    DEFAULT NULL COMMENT '整单折扣',
+    `product_price`       int                                                                    DEFAULT NULL COMMENT '产品总金额',
+    `ro_user_ids`         varchar(4096)                                                          DEFAULT NULL COMMENT '只读权限的用户编号数组',
+    `rw_user_ids`         varchar(4096)                                                          DEFAULT NULL COMMENT '读写权限的用户编号数组',
+    `contact_id`          bigint                                                                 DEFAULT NULL COMMENT '联系人编号',
+    `sign_user_id`        bigint                                                                 DEFAULT NULL COMMENT '公司签约人',
+    `contact_last_time`   datetime                                                               DEFAULT NULL COMMENT '最后跟进时间',
+
+    -- 通用字段
+    `remark`              varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL     DEFAULT NULL COMMENT '备注',
+    `creator`             varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci  NULL     DEFAULT '' COMMENT '创建者',
+    `create_time`         datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updater`             varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci  NULL     DEFAULT '' COMMENT '更新者',
+    `update_time`         datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `deleted`             bit(1)                                                        NOT NULL DEFAULT b'0' COMMENT '是否删除',
+    `tenant_id`           bigint                                                        NOT NULL DEFAULT 0 COMMENT '租户编号',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB
+  CHARACTER SET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='合同表';
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
new file mode 100644
index 000000000..83e5ae29d
--- /dev/null
+++ b/sql/mysql/crm_menu.sql
@@ -0,0 +1,63 @@
+-- ----------------------------
+-- 合同菜单
+-- ----------------------------
+
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+    '合同管理', '', 2, 0, 1254,
+    'contract', '', 'crm/contract/index', 0, 'Contract'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '合同查询', 'crm:contract:query', 3, 1, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '合同创建', 'crm:contract:create', 3, 2, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '合同更新', 'crm:contract:update', 3, 3, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '合同删除', 'crm:contract:delete', 3, 4, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '合同导出', 'crm:contract:export', 3, 5, @parentId,
+    '', '', '', 0
+);
+
+-- ----------------------------
+-- ...菜单
+-- ----------------------------
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
new file mode 100644
index 000000000..c45d2602e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
@@ -0,0 +1 @@
+DELETE FROM "crm_contract";
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
new file mode 100644
index 000000000..c25a80e44
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -0,0 +1,27 @@
+CREATE TABLE IF NOT EXISTS "crm_contract" (
+  "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+  "name" varchar NOT NULL,
+  "customer_id" bigint,
+  "business_id" bigint,
+  "process_instance_id" bigint,
+  "order_date" varchar,
+  "owner_user_id" bigint,
+  "no" varchar,
+  "start_time" varchar,
+  "end_time" varchar,
+  "price" int,
+  "discount_percent" int,
+  "product_price" int,
+  "ro_user_ids" varchar,
+  "rw_user_ids" varchar,
+  "contact_id" bigint,
+  "sign_user_id" bigint,
+  "contact_last_time" varchar,
+  "remark" varchar,
+  "creator" varchar DEFAULT '',
+  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "updater" varchar DEFAULT '',
+  "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  "deleted" bit NOT NULL DEFAULT FALSE,
+  PRIMARY KEY ("id")
+    ) COMMENT '合同表';
\ No newline at end of file

From b133acd8cdd741ded5bce27225ba12fefc3f2da2 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Tue, 17 Oct 2023 22:52:14 +0800
Subject: [PATCH 006/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E5=90=88?=
 =?UTF-8?q?=E5=90=8C=E6=B5=81=E7=A8=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/admin/contract/ContractController.java  | 10 ----------
 .../controller/admin/contract/vo/ContractBaseVO.java   | 10 ++++++++++
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
index 3bc52c038..fd8b14afd 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
@@ -19,7 +19,6 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.IOException;
-import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -67,15 +66,6 @@ public class ContractController {
         return success(ContractConvert.INSTANCE.convert(contract));
     }
 
-    @GetMapping("/list")
-    @Operation(summary = "获得合同列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
-    public CommonResult<List<ContractRespVO>> getContractList(@RequestParam("ids") Collection<Long> ids) {
-        List<ContractDO> list = contractService.getContractList(ids);
-        return success(ContractConvert.INSTANCE.convertList(list));
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得合同分页")
     @PreAuthorize("@ss.hasPermission('crm:contract:query')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java
index ad43604a1..756ee0d1f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java
@@ -9,6 +9,7 @@ import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
+// TODO @dhb52:所有类,带下 Crm 前缀,避免和别的模块重复
 /**
  * 合同 Base VO,提供给添加、修改、详细的子 VO 使用
  * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
@@ -16,10 +17,13 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class ContractBaseVO {
 
+    // TODO @dhb52:类似 no 字段的 example 要写xia 哈;
+
     @Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
     @NotNull(message = "合同名称不能为空")
     private String name;
 
+    // TODO @dhb52:这个必须传递
     @Schema(description = "客户编号", example = "18336")
     private Long customerId;
 
@@ -29,13 +33,17 @@ public class ContractBaseVO {
     @Schema(description = "工作流编号", example = "1043")
     private Long processInstanceId;
 
+    // TODO @dhb52:这个必须传递
     @Schema(description = "下单日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime orderDate;
 
+    // TODO @dhb52:这个必须传递
     @Schema(description = "负责人的用户编号", example = "17144")
     private Long ownerUserId;
 
+    // TODO @芋艿:未来应该支持自动生成;
+    // TODO @dhb52:这个必须传递;
     @Schema(description = "合同编号")
     private String no;
 
@@ -69,4 +77,6 @@ public class ContractBaseVO {
     @Schema(description = "备注", example = "你猜")
     private String remark;
 
+    // TODO @dhb52:增加一个 status 字段:具体有哪些值,你来枚举下;主要页面上有个【草稿】【提交审核】的流程,可以看看。然后要对接工作流,这块也可以看看,不确定的地方问我。
+
 }

From a09ee495b674411d312dce4e18b29360b6ec7e38 Mon Sep 17 00:00:00 2001
From: "913752709@qq.com" <913752709@qq.com>
Date: Wed, 18 Oct 2023 19:53:15 +0800
Subject: [PATCH 007/101] =?UTF-8?q?feat:=20CRM=20=E7=BA=BF=E7=B4=A2?=
 =?UTF-8?q?=E8=A1=A8=20crud?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  24 ++
 sql/mysql/crm_menu.sql                        |  60 ++++-
 .../module/crm/enums/ErrorCodeConstants.java  |   2 +
 .../admin/clue/CrmClueController.java         | 102 ++++++++
 .../admin/clue/vo/CrmClueBaseVO.java          |  64 +++++
 .../admin/clue/vo/CrmClueCreateReqVO.java     |  14 ++
 .../admin/clue/vo/CrmClueExcelVO.java         |  65 +++++
 .../admin/clue/vo/CrmClueExportReqVO.java     |  52 ++++
 .../admin/clue/vo/CrmCluePageReqVO.java       |  54 ++++
 .../admin/clue/vo/CrmClueRespVO.java          |  19 ++
 .../admin/clue/vo/CrmClueUpdateReqVO.java     |  18 ++
 .../crm/convert/clue/CrmClueConvert.java      |  34 +++
 .../crm/dal/dataobject/clue/CrmClueDO.java    |  81 ++++++
 .../crm/dal/mysql/clue/CrmClueMapper.java     |  52 ++++
 .../crm/service/clue/CrmClueService.java      |  70 ++++++
 .../crm/service/clue/CrmClueServiceImpl.java  |  88 +++++++
 .../resources/mapper/clue/CrmClueMapper.xml   |  12 +
 .../service/clue/CrmClueServiceImplTest.java  | 231 ++++++++++++++++++
 .../src/test/resources/sql/clean.sql          |   2 +
 .../src/test/resources/sql/create_tables.sql  |  24 +-
 20 files changed, 1065 insertions(+), 3 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index eeb2d12ac..6c5594068 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -37,3 +37,27 @@ CREATE TABLE `crm_contract`
 ) ENGINE = InnoDB
   CHARACTER SET = utf8mb4
   COLLATE = utf8mb4_unicode_ci COMMENT ='合同表';
+
+
+DROP TABLE IF EXISTS `crm_clue`;
+CREATE TABLE `crm_clue`  (
+     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
+     `transform_status` tinyint NOT NULL COMMENT '转化状态',
+     `follow_up_status` tinyint NOT NULL COMMENT '跟进状态',
+     `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '线索名称',
+     `customer_id` bigint NOT NULL COMMENT '客户id',
+     `contact_next_time` datetime NULL DEFAULT NULL COMMENT '下次联系时间',
+     `telephone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电话',
+     `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
+     `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地址',
+     `owner_user_id` bigint NULL DEFAULT NULL COMMENT '负责人的用户编号',
+     `contact_last_time` datetime NULL DEFAULT NULL COMMENT '最后跟进时间',
+     `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
+     `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+     `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+     `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+     `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+     `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+     `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+     PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
\ No newline at end of file
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index 83e5ae29d..cf0fe00fd 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -59,5 +59,61 @@ VALUES (
 );
 
 -- ----------------------------
--- ...菜单
--- ----------------------------
\ No newline at end of file
+-- 线索菜单
+-- ----------------------------
+
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+   '线索管理', '', 2, 0, 2375,
+   'clue', '', 'crm/clue/index', 0, 'CrmClue'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索查询', 'crm:clue:query', 3, 1, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索创建', 'crm:clue:create', 3, 2, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索更新', 'crm:clue:update', 3, 3, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索删除', 'crm:clue:delete', 3, 4, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索导出', 'crm:clue:export', 3, 5, @parentId,
+   '', '', '', 0
+);
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 532533d6c..85eb929fc 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -12,4 +12,6 @@ public interface ErrorCodeConstants {
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
 
+    ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_000_001, "线索不存在");
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
new file mode 100644
index 000000000..a1979b99a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
@@ -0,0 +1,102 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
+import cn.iocoder.yudao.module.crm.service.clue.CrmClueService;
+
+@Tag(name = "管理后台 - 线索")
+@RestController
+@RequestMapping("/crm/clue")
+@Validated
+public class CrmClueController {
+
+    @Resource
+    private CrmClueService clueService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建线索")
+    @PreAuthorize("@ss.hasPermission('crm:clue:create')")
+    public CommonResult<Long> createClue(@Valid @RequestBody CrmClueCreateReqVO createReqVO) {
+        return success(clueService.createClue(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新线索")
+    @PreAuthorize("@ss.hasPermission('crm:clue:update')")
+    public CommonResult<Boolean> updateClue(@Valid @RequestBody CrmClueUpdateReqVO updateReqVO) {
+        clueService.updateClue(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除线索")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:clue:delete')")
+    public CommonResult<Boolean> deleteClue(@RequestParam("id") Long id) {
+        clueService.deleteClue(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得线索")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<CrmClueRespVO> getClue(@RequestParam("id") Long id) {
+        CrmClueDO clue = clueService.getClue(id);
+        return success(CrmClueConvert.INSTANCE.convert(clue));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得线索列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<List<CrmClueRespVO>> getClueList(@RequestParam("ids") Collection<Long> ids) {
+        List<CrmClueDO> list = clueService.getClueList(ids);
+        return success(CrmClueConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得线索分页")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<PageResult<CrmClueRespVO>> getCluePage(@Valid CrmCluePageReqVO pageVO) {
+        PageResult<CrmClueDO> pageResult = clueService.getCluePage(pageVO);
+        return success(CrmClueConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出线索 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:clue:export')")
+    @OperateLog(type = EXPORT)
+    public void exportClueExcel(@Valid CrmClueExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmClueDO> list = clueService.getClueList(exportReqVO);
+        // 导出 Excel
+        List<CrmClueExcelVO> datas = CrmClueConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "线索.xls", "数据", CrmClueExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
new file mode 100644
index 000000000..285f8029f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import cn.iocoder.yudao.framework.common.validation.Mobile;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import javax.validation.constraints.*;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 线索 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmClueBaseVO {
+
+    @Schema(description = "转化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "转化状态不能为空")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "跟进状态不能为空")
+    private Boolean followUpStatus;
+
+    @Schema(description = "线索名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "线索xxx")
+    @NotNull(message = "线索名称不能为空")
+    private String name;
+
+    @Schema(description = "客户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "520")
+    @NotNull(message = "客户id不能为空")
+    private Long customerId;
+
+    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactNextTime;
+
+    @Mobile(message = "电话格式不正确")
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Mobile(message = "手机号格式不正确")
+    @Schema(description = "手机号", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @Schema(description = "负责人的用户编号", example = "27199")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java
new file mode 100644
index 000000000..0d43e15a6
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 线索创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueCreateReqVO extends CrmClueBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
new file mode 100644
index 000000000..13bc9c42b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+
+/**
+ * 线索 Excel VO
+ *
+ * @author Wanwan
+ */
+@Data
+public class CrmClueExcelVO {
+
+    @ExcelProperty("编号,主键自增")
+    private Long id;
+
+    @ExcelProperty(value = "转化状态", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Boolean transformStatus;
+
+    @ExcelProperty(value = "跟进状态", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Boolean followUpStatus;
+
+    @ExcelProperty("线索名称")
+    private String name;
+
+    @ExcelProperty("客户id")
+    private Long customerId;
+
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("手机号")
+    private String mobile;
+
+    @ExcelProperty("地址")
+    private String address;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java
new file mode 100644
index 000000000..fe061b365
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import java.time.LocalDateTime;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 线索 Excel 导出 Request VO,参数和 CrmCluePageReqVO 是一致的")
+@Data
+public class CrmClueExportReqVO {
+
+    @Schema(description = "转化状态", example = "true")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", example = "true")
+    private Boolean followUpStatus;
+
+    @Schema(description = "线索名称", example = "线索xxx")
+    private String name;
+
+    @Schema(description = "客户id", example = "520")
+    private Long customerId;
+
+    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactNextTime;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Schema(description = "手机号", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @Schema(description = "负责人的用户编号", example = "27199")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactLastTime;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
new file mode 100644
index 000000000..b9cf71c42
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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 CrmCluePageReqVO extends PageParam {
+
+    @Schema(description = "转化状态", example = "true")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", example = "true")
+    private Boolean followUpStatus;
+
+    @Schema(description = "线索名称", example = "线索xxx")
+    private String name;
+
+    @Schema(description = "客户id", example = "520")
+    private Long customerId;
+
+    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactNextTime;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Schema(description = "手机号", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @Schema(description = "负责人的用户编号", example = "27199")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactLastTime;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
new file mode 100644
index 000000000..60eb2939b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 线索 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueRespVO extends CrmClueBaseVO {
+
+    @Schema(description = "编号,主键自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "10969")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
new file mode 100644
index 000000000..2a264de97
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 线索更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueUpdateReqVO extends CrmClueBaseVO {
+
+    @Schema(description = "编号,主键自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "10969")
+    @NotNull(message = "编号,主键自增不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
new file mode 100644
index 000000000..144a77da6
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.clue;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+
+/**
+ * 线索 Convert
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmClueConvert {
+
+    CrmClueConvert INSTANCE = Mappers.getMapper(CrmClueConvert.class);
+
+    CrmClueDO convert(CrmClueCreateReqVO bean);
+
+    CrmClueDO convert(CrmClueUpdateReqVO bean);
+
+    CrmClueRespVO convert(CrmClueDO bean);
+
+    List<CrmClueRespVO> convertList(List<CrmClueDO> list);
+
+    PageResult<CrmClueRespVO> convertPage(PageResult<CrmClueDO> page);
+
+    List<CrmClueExcelVO> convertList02(List<CrmClueDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
new file mode 100644
index 000000000..22e1b8912
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.clue;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 线索 DO
+ *
+ * @author Wanwan
+ */
+@TableName("crm_clue")
+@KeySequence("crm_clue_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmClueDO extends BaseDO {
+
+    /**
+     * 编号,主键自增
+     */
+    @TableId
+    private Long id;
+    /**
+     * 转化状态
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean transformStatus;
+    /**
+     * 跟进状态
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean followUpStatus;
+    /**
+     * 线索名称
+     */
+    private String name;
+    /**
+     * 客户id
+     */
+    private Long customerId;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime contactNextTime;
+    /**
+     * 电话
+     */
+    private String telephone;
+    /**
+     * 手机号
+     */
+    private String mobile;
+    /**
+     * 地址
+     */
+    private String address;
+    /**
+     * 负责人的用户编号
+     */
+    private Long ownerUserId;
+    /**
+     * 最后跟进时间
+     */
+    private LocalDateTime contactLastTime;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
new file mode 100644
index 000000000..044615bcf
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.clue;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+
+/**
+ * 线索 Mapper
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
+
+    default PageResult<CrmClueDO> selectPage(CrmCluePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmClueDO>()
+                .eqIfPresent(CrmClueDO::getTransformStatus, reqVO.getTransformStatus())
+                .eqIfPresent(CrmClueDO::getFollowUpStatus, reqVO.getFollowUpStatus())
+                .likeIfPresent(CrmClueDO::getName, reqVO.getName())
+                .eqIfPresent(CrmClueDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(CrmClueDO::getContactNextTime, reqVO.getContactNextTime())
+                .likeIfPresent(CrmClueDO::getTelephone, reqVO.getTelephone())
+                .likeIfPresent(CrmClueDO::getMobile, reqVO.getMobile())
+                .likeIfPresent(CrmClueDO::getAddress, reqVO.getAddress())
+                .eqIfPresent(CrmClueDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(CrmClueDO::getContactLastTime, reqVO.getContactLastTime())
+                .betweenIfPresent(CrmClueDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmClueDO::getId));
+    }
+
+    default List<CrmClueDO> selectList(CrmClueExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CrmClueDO>()
+                .eqIfPresent(CrmClueDO::getTransformStatus, reqVO.getTransformStatus())
+                .eqIfPresent(CrmClueDO::getFollowUpStatus, reqVO.getFollowUpStatus())
+                .likeIfPresent(CrmClueDO::getName, reqVO.getName())
+                .eqIfPresent(CrmClueDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(CrmClueDO::getContactNextTime, reqVO.getContactNextTime())
+                .likeIfPresent(CrmClueDO::getTelephone, reqVO.getTelephone())
+                .likeIfPresent(CrmClueDO::getMobile, reqVO.getMobile())
+                .likeIfPresent(CrmClueDO::getAddress, reqVO.getAddress())
+                .eqIfPresent(CrmClueDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(CrmClueDO::getContactLastTime, reqVO.getContactLastTime())
+                .betweenIfPresent(CrmClueDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmClueDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java
new file mode 100644
index 000000000..3bf627f5f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.crm.service.clue;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 线索 Service 接口
+ *
+ * @author Wanwan
+ */
+public interface CrmClueService {
+
+    /**
+     * 创建线索
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createClue(@Valid CrmClueCreateReqVO createReqVO);
+
+    /**
+     * 更新线索
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateClue(@Valid CrmClueUpdateReqVO updateReqVO);
+
+    /**
+     * 删除线索
+     *
+     * @param id 编号
+     */
+    void deleteClue(Long id);
+
+    /**
+     * 获得线索
+     *
+     * @param id 编号
+     * @return 线索
+     */
+    CrmClueDO getClue(Long id);
+
+    /**
+     * 获得线索列表
+     *
+     * @param ids 编号
+     * @return 线索列表
+     */
+    List<CrmClueDO> getClueList(Collection<Long> ids);
+
+    /**
+     * 获得线索分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 线索分页
+     */
+    PageResult<CrmClueDO> getCluePage(CrmCluePageReqVO pageReqVO);
+
+    /**
+     * 获得线索列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 线索列表
+     */
+    List<CrmClueDO> getClueList(CrmClueExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
new file mode 100644
index 000000000..92193361f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.crm.service.clue;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
+import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+
+/**
+ * 线索 Service 实现类
+ *
+ * @author Wanwan
+ */
+@Service
+@Validated
+public class CrmClueServiceImpl implements CrmClueService {
+
+    @Resource
+    private CrmClueMapper clueMapper;
+
+    @Override
+    public Long createClue(CrmClueCreateReqVO createReqVO) {
+        // 插入
+        CrmClueDO clue = CrmClueConvert.INSTANCE.convert(createReqVO);
+        clueMapper.insert(clue);
+        // 返回
+        return clue.getId();
+    }
+
+    @Override
+    public void updateClue(CrmClueUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateClueExists(updateReqVO.getId());
+        // 更新
+        CrmClueDO updateObj = CrmClueConvert.INSTANCE.convert(updateReqVO);
+        clueMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteClue(Long id) {
+        // 校验存在
+        validateClueExists(id);
+        // 删除
+        clueMapper.deleteById(id);
+    }
+
+    private void validateClueExists(Long id) {
+        if (clueMapper.selectById(id) == null) {
+            throw exception(CLUE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmClueDO getClue(Long id) {
+        return clueMapper.selectById(id);
+    }
+
+    @Override
+    public List<CrmClueDO> getClueList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return clueMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CrmClueDO> getCluePage(CrmCluePageReqVO pageReqVO) {
+        return clueMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CrmClueDO> getClueList(CrmClueExportReqVO exportReqVO) {
+        return clueMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
new file mode 100644
index 000000000..3f15cf766
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
new file mode 100644
index 000000000..ce68734c1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
@@ -0,0 +1,231 @@
+package cn.iocoder.yudao.module.crm.service.clue;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link CrmClueServiceImpl} 的单元测试类
+ *
+ * @author Wanwan
+ */
+@Import(CrmClueServiceImpl.class)
+public class CrmClueServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmClueServiceImpl clueService;
+
+    @Resource
+    private CrmClueMapper clueMapper;
+
+    @Test
+    public void testCreateClue_success() {
+        // 准备参数
+        CrmClueCreateReqVO reqVO = randomPojo(CrmClueCreateReqVO.class);
+
+        // 调用
+        Long clueId = clueService.createClue(reqVO);
+        // 断言
+        assertNotNull(clueId);
+        // 校验记录的属性是否正确
+        CrmClueDO clue = clueMapper.selectById(clueId);
+        assertPojoEquals(reqVO, clue);
+    }
+
+    @Test
+    public void testUpdateClue_success() {
+        // mock 数据
+        CrmClueDO dbClue = randomPojo(CrmClueDO.class);
+        clueMapper.insert(dbClue);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmClueUpdateReqVO reqVO = randomPojo(CrmClueUpdateReqVO.class, o -> {
+            o.setId(dbClue.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        clueService.updateClue(reqVO);
+        // 校验是否更新正确
+        CrmClueDO clue = clueMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, clue);
+    }
+
+    @Test
+    public void testUpdateClue_notExists() {
+        // 准备参数
+        CrmClueUpdateReqVO reqVO = randomPojo(CrmClueUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> clueService.updateClue(reqVO), CLUE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteClue_success() {
+        // mock 数据
+        CrmClueDO dbClue = randomPojo(CrmClueDO.class);
+        clueMapper.insert(dbClue);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbClue.getId();
+
+        // 调用
+        clueService.deleteClue(id);
+       // 校验数据不存在了
+       assertNull(clueMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteClue_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> clueService.deleteClue(id), CLUE_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetCluePage() {
+       // mock 数据
+       CrmClueDO dbClue = randomPojo(CrmClueDO.class, o -> { // 等会查询到
+           o.setTransformStatus(null);
+           o.setFollowUpStatus(null);
+           o.setName(null);
+           o.setCustomerId(null);
+           o.setContactNextTime(null);
+           o.setTelephone(null);
+           o.setMobile(null);
+           o.setAddress(null);
+           o.setOwnerUserId(null);
+           o.setContactLastTime(null);
+           o.setCreateTime(null);
+       });
+       clueMapper.insert(dbClue);
+       // 测试 transformStatus 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setTransformStatus(null)));
+       // 测试 followUpStatus 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setFollowUpStatus(null)));
+       // 测试 name 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setName(null)));
+       // 测试 customerId 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCustomerId(null)));
+       // 测试 contactNextTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactNextTime(null)));
+       // 测试 telephone 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setTelephone(null)));
+       // 测试 mobile 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setMobile(null)));
+       // 测试 address 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setAddress(null)));
+       // 测试 ownerUserId 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setOwnerUserId(null)));
+       // 测试 contactLastTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactLastTime(null)));
+       // 测试 createTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmCluePageReqVO reqVO = new CrmCluePageReqVO();
+       reqVO.setTransformStatus(null);
+       reqVO.setFollowUpStatus(null);
+       reqVO.setName(null);
+       reqVO.setCustomerId(null);
+       reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setTelephone(null);
+       reqVO.setMobile(null);
+       reqVO.setAddress(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<CrmClueDO> pageResult = clueService.getCluePage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbClue, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetClueList() {
+       // mock 数据
+       CrmClueDO dbClue = randomPojo(CrmClueDO.class, o -> { // 等会查询到
+           o.setTransformStatus(null);
+           o.setFollowUpStatus(null);
+           o.setName(null);
+           o.setCustomerId(null);
+           o.setContactNextTime(null);
+           o.setTelephone(null);
+           o.setMobile(null);
+           o.setAddress(null);
+           o.setOwnerUserId(null);
+           o.setContactLastTime(null);
+           o.setCreateTime(null);
+       });
+       clueMapper.insert(dbClue);
+       // 测试 transformStatus 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setTransformStatus(null)));
+       // 测试 followUpStatus 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setFollowUpStatus(null)));
+       // 测试 name 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setName(null)));
+       // 测试 customerId 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCustomerId(null)));
+       // 测试 contactNextTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactNextTime(null)));
+       // 测试 telephone 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setTelephone(null)));
+       // 测试 mobile 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setMobile(null)));
+       // 测试 address 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setAddress(null)));
+       // 测试 ownerUserId 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setOwnerUserId(null)));
+       // 测试 contactLastTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactLastTime(null)));
+       // 测试 createTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmClueExportReqVO reqVO = new CrmClueExportReqVO();
+       reqVO.setTransformStatus(null);
+       reqVO.setFollowUpStatus(null);
+       reqVO.setName(null);
+       reqVO.setCustomerId(null);
+       reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setTelephone(null);
+       reqVO.setMobile(null);
+       reqVO.setAddress(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       List<CrmClueDO> list = clueService.getClueList(reqVO);
+       // 断言
+       assertEquals(1, list.size());
+       assertPojoEquals(dbClue, list.get(0));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
index c45d2602e..983e53ab3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
@@ -1 +1,3 @@
 DELETE FROM "crm_contract";
+
+DELETE FROM "crm_clue";
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
index c25a80e44..816264e66 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -24,4 +24,26 @@ CREATE TABLE IF NOT EXISTS "crm_contract" (
   "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   "deleted" bit NOT NULL DEFAULT FALSE,
   PRIMARY KEY ("id")
-    ) COMMENT '合同表';
\ No newline at end of file
+) COMMENT '合同表';
+
+CREATE TABLE IF NOT EXISTS "crm_clue" (
+  "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+  "transform_status" bit NOT NULL,
+  "follow_up_status" bit NOT NULL,
+  "name" varchar NOT NULL,
+  "customer_id" bigint NOT NULL,
+  "contact_next_time" varchar,
+  "telephone" varchar,
+  "mobile" varchar,
+  "address" varchar,
+  "owner_user_id" bigint,
+  "contact_last_time" varchar,
+  "remark" varchar,
+  "creator" varchar DEFAULT '',
+  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "updater" varchar DEFAULT '',
+  "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  "deleted" bit NOT NULL DEFAULT FALSE,
+  "tenant_id" bigint NOT NULL,
+  PRIMARY KEY ("id")
+) COMMENT '线索表';
\ No newline at end of file

From 1b6c8574cde00a79675b90d6e49042c61f55e495 Mon Sep 17 00:00:00 2001
From: "913752709@qq.com" <913752709@qq.com>
Date: Wed, 18 Oct 2023 20:02:53 +0800
Subject: [PATCH 008/101] =?UTF-8?q?feat:=20CRM=20=E7=BA=BF=E7=B4=A2?=
 =?UTF-8?q?=E8=A1=A8=20crud?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../yudao/module/crm/dal/dataobject/clue/CrmClueDO.java        | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
index 22e1b8912..986d933e8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.clue;
 
+import com.sun.xml.bind.v2.TODO;
 import lombok.*;
 import java.util.*;
 import java.time.LocalDateTime;
@@ -70,7 +71,7 @@ public class CrmClueDO extends BaseDO {
      */
     private Long ownerUserId;
     /**
-     * 最后跟进时间
+     * 最后跟进时间 TODO 添加跟进记录时更新该值
      */
     private LocalDateTime contactLastTime;
     /**

From 48661f980c59c92f66e33ce2a74be4e3abec4e62 Mon Sep 17 00:00:00 2001
From: wanwan <913752709@qq.com>
Date: Thu, 19 Oct 2023 14:52:16 +0000
Subject: [PATCH 009/101] =?UTF-8?q?!679=20feat:=20CRM=20=E7=BA=BF=E7=B4=A2?=
 =?UTF-8?q?=E8=A1=A8=20crud=20*=20feat:=20CRM=20=E7=BA=BF=E7=B4=A2?=
 =?UTF-8?q?=E8=A1=A8=20crud=20*=20feat:=20CRM=20=E7=BA=BF=E7=B4=A2?=
 =?UTF-8?q?=E8=A1=A8=20crud?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  24 ++
 sql/mysql/crm_menu.sql                        |  60 ++++-
 .../module/crm/enums/ErrorCodeConstants.java  |   2 +
 .../admin/clue/CrmClueController.java         | 102 ++++++++
 .../admin/clue/vo/CrmClueBaseVO.java          |  64 +++++
 .../admin/clue/vo/CrmClueCreateReqVO.java     |  14 ++
 .../admin/clue/vo/CrmClueExcelVO.java         |  65 +++++
 .../admin/clue/vo/CrmClueExportReqVO.java     |  52 ++++
 .../admin/clue/vo/CrmCluePageReqVO.java       |  54 ++++
 .../admin/clue/vo/CrmClueRespVO.java          |  19 ++
 .../admin/clue/vo/CrmClueUpdateReqVO.java     |  18 ++
 .../crm/convert/clue/CrmClueConvert.java      |  34 +++
 .../crm/dal/dataobject/clue/CrmClueDO.java    |  82 +++++++
 .../crm/dal/mysql/clue/CrmClueMapper.java     |  52 ++++
 .../crm/service/clue/CrmClueService.java      |  70 ++++++
 .../crm/service/clue/CrmClueServiceImpl.java  |  88 +++++++
 .../resources/mapper/clue/CrmClueMapper.xml   |  12 +
 .../service/clue/CrmClueServiceImplTest.java  | 231 ++++++++++++++++++
 .../src/test/resources/sql/clean.sql          |   2 +
 .../src/test/resources/sql/create_tables.sql  |  24 +-
 20 files changed, 1066 insertions(+), 3 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index eeb2d12ac..6c5594068 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -37,3 +37,27 @@ CREATE TABLE `crm_contract`
 ) ENGINE = InnoDB
   CHARACTER SET = utf8mb4
   COLLATE = utf8mb4_unicode_ci COMMENT ='合同表';
+
+
+DROP TABLE IF EXISTS `crm_clue`;
+CREATE TABLE `crm_clue`  (
+     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
+     `transform_status` tinyint NOT NULL COMMENT '转化状态',
+     `follow_up_status` tinyint NOT NULL COMMENT '跟进状态',
+     `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '线索名称',
+     `customer_id` bigint NOT NULL COMMENT '客户id',
+     `contact_next_time` datetime NULL DEFAULT NULL COMMENT '下次联系时间',
+     `telephone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电话',
+     `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
+     `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地址',
+     `owner_user_id` bigint NULL DEFAULT NULL COMMENT '负责人的用户编号',
+     `contact_last_time` datetime NULL DEFAULT NULL COMMENT '最后跟进时间',
+     `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
+     `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+     `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+     `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+     `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+     `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+     `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+     PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
\ No newline at end of file
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index 83e5ae29d..cf0fe00fd 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -59,5 +59,61 @@ VALUES (
 );
 
 -- ----------------------------
--- ...菜单
--- ----------------------------
\ No newline at end of file
+-- 线索菜单
+-- ----------------------------
+
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+   '线索管理', '', 2, 0, 2375,
+   'clue', '', 'crm/clue/index', 0, 'CrmClue'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索查询', 'crm:clue:query', 3, 1, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索创建', 'crm:clue:create', 3, 2, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索更新', 'crm:clue:update', 3, 3, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索删除', 'crm:clue:delete', 3, 4, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '线索导出', 'crm:clue:export', 3, 5, @parentId,
+   '', '', '', 0
+);
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 532533d6c..85eb929fc 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -12,4 +12,6 @@ public interface ErrorCodeConstants {
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
 
+    ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_000_001, "线索不存在");
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
new file mode 100644
index 000000000..a1979b99a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
@@ -0,0 +1,102 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
+import cn.iocoder.yudao.module.crm.service.clue.CrmClueService;
+
+@Tag(name = "管理后台 - 线索")
+@RestController
+@RequestMapping("/crm/clue")
+@Validated
+public class CrmClueController {
+
+    @Resource
+    private CrmClueService clueService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建线索")
+    @PreAuthorize("@ss.hasPermission('crm:clue:create')")
+    public CommonResult<Long> createClue(@Valid @RequestBody CrmClueCreateReqVO createReqVO) {
+        return success(clueService.createClue(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新线索")
+    @PreAuthorize("@ss.hasPermission('crm:clue:update')")
+    public CommonResult<Boolean> updateClue(@Valid @RequestBody CrmClueUpdateReqVO updateReqVO) {
+        clueService.updateClue(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除线索")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:clue:delete')")
+    public CommonResult<Boolean> deleteClue(@RequestParam("id") Long id) {
+        clueService.deleteClue(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得线索")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<CrmClueRespVO> getClue(@RequestParam("id") Long id) {
+        CrmClueDO clue = clueService.getClue(id);
+        return success(CrmClueConvert.INSTANCE.convert(clue));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得线索列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<List<CrmClueRespVO>> getClueList(@RequestParam("ids") Collection<Long> ids) {
+        List<CrmClueDO> list = clueService.getClueList(ids);
+        return success(CrmClueConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得线索分页")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<PageResult<CrmClueRespVO>> getCluePage(@Valid CrmCluePageReqVO pageVO) {
+        PageResult<CrmClueDO> pageResult = clueService.getCluePage(pageVO);
+        return success(CrmClueConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出线索 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:clue:export')")
+    @OperateLog(type = EXPORT)
+    public void exportClueExcel(@Valid CrmClueExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmClueDO> list = clueService.getClueList(exportReqVO);
+        // 导出 Excel
+        List<CrmClueExcelVO> datas = CrmClueConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "线索.xls", "数据", CrmClueExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
new file mode 100644
index 000000000..285f8029f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import cn.iocoder.yudao.framework.common.validation.Mobile;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import javax.validation.constraints.*;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 线索 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmClueBaseVO {
+
+    @Schema(description = "转化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "转化状态不能为空")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "跟进状态不能为空")
+    private Boolean followUpStatus;
+
+    @Schema(description = "线索名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "线索xxx")
+    @NotNull(message = "线索名称不能为空")
+    private String name;
+
+    @Schema(description = "客户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "520")
+    @NotNull(message = "客户id不能为空")
+    private Long customerId;
+
+    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactNextTime;
+
+    @Mobile(message = "电话格式不正确")
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Mobile(message = "手机号格式不正确")
+    @Schema(description = "手机号", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @Schema(description = "负责人的用户编号", example = "27199")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java
new file mode 100644
index 000000000..0d43e15a6
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 线索创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueCreateReqVO extends CrmClueBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
new file mode 100644
index 000000000..13bc9c42b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+
+/**
+ * 线索 Excel VO
+ *
+ * @author Wanwan
+ */
+@Data
+public class CrmClueExcelVO {
+
+    @ExcelProperty("编号,主键自增")
+    private Long id;
+
+    @ExcelProperty(value = "转化状态", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Boolean transformStatus;
+
+    @ExcelProperty(value = "跟进状态", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Boolean followUpStatus;
+
+    @ExcelProperty("线索名称")
+    private String name;
+
+    @ExcelProperty("客户id")
+    private Long customerId;
+
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("手机号")
+    private String mobile;
+
+    @ExcelProperty("地址")
+    private String address;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java
new file mode 100644
index 000000000..fe061b365
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import java.time.LocalDateTime;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 线索 Excel 导出 Request VO,参数和 CrmCluePageReqVO 是一致的")
+@Data
+public class CrmClueExportReqVO {
+
+    @Schema(description = "转化状态", example = "true")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", example = "true")
+    private Boolean followUpStatus;
+
+    @Schema(description = "线索名称", example = "线索xxx")
+    private String name;
+
+    @Schema(description = "客户id", example = "520")
+    private Long customerId;
+
+    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactNextTime;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Schema(description = "手机号", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @Schema(description = "负责人的用户编号", example = "27199")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactLastTime;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
new file mode 100644
index 000000000..b9cf71c42
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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 CrmCluePageReqVO extends PageParam {
+
+    @Schema(description = "转化状态", example = "true")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", example = "true")
+    private Boolean followUpStatus;
+
+    @Schema(description = "线索名称", example = "线索xxx")
+    private String name;
+
+    @Schema(description = "客户id", example = "520")
+    private Long customerId;
+
+    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactNextTime;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Schema(description = "手机号", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @Schema(description = "负责人的用户编号", example = "27199")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactLastTime;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
new file mode 100644
index 000000000..60eb2939b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 线索 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueRespVO extends CrmClueBaseVO {
+
+    @Schema(description = "编号,主键自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "10969")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
new file mode 100644
index 000000000..2a264de97
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 线索更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueUpdateReqVO extends CrmClueBaseVO {
+
+    @Schema(description = "编号,主键自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "10969")
+    @NotNull(message = "编号,主键自增不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
new file mode 100644
index 000000000..144a77da6
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.clue;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+
+/**
+ * 线索 Convert
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmClueConvert {
+
+    CrmClueConvert INSTANCE = Mappers.getMapper(CrmClueConvert.class);
+
+    CrmClueDO convert(CrmClueCreateReqVO bean);
+
+    CrmClueDO convert(CrmClueUpdateReqVO bean);
+
+    CrmClueRespVO convert(CrmClueDO bean);
+
+    List<CrmClueRespVO> convertList(List<CrmClueDO> list);
+
+    PageResult<CrmClueRespVO> convertPage(PageResult<CrmClueDO> page);
+
+    List<CrmClueExcelVO> convertList02(List<CrmClueDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
new file mode 100644
index 000000000..986d933e8
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.clue;
+
+import com.sun.xml.bind.v2.TODO;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 线索 DO
+ *
+ * @author Wanwan
+ */
+@TableName("crm_clue")
+@KeySequence("crm_clue_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmClueDO extends BaseDO {
+
+    /**
+     * 编号,主键自增
+     */
+    @TableId
+    private Long id;
+    /**
+     * 转化状态
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean transformStatus;
+    /**
+     * 跟进状态
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean followUpStatus;
+    /**
+     * 线索名称
+     */
+    private String name;
+    /**
+     * 客户id
+     */
+    private Long customerId;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime contactNextTime;
+    /**
+     * 电话
+     */
+    private String telephone;
+    /**
+     * 手机号
+     */
+    private String mobile;
+    /**
+     * 地址
+     */
+    private String address;
+    /**
+     * 负责人的用户编号
+     */
+    private Long ownerUserId;
+    /**
+     * 最后跟进时间 TODO 添加跟进记录时更新该值
+     */
+    private LocalDateTime contactLastTime;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
new file mode 100644
index 000000000..044615bcf
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.clue;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+
+/**
+ * 线索 Mapper
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
+
+    default PageResult<CrmClueDO> selectPage(CrmCluePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmClueDO>()
+                .eqIfPresent(CrmClueDO::getTransformStatus, reqVO.getTransformStatus())
+                .eqIfPresent(CrmClueDO::getFollowUpStatus, reqVO.getFollowUpStatus())
+                .likeIfPresent(CrmClueDO::getName, reqVO.getName())
+                .eqIfPresent(CrmClueDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(CrmClueDO::getContactNextTime, reqVO.getContactNextTime())
+                .likeIfPresent(CrmClueDO::getTelephone, reqVO.getTelephone())
+                .likeIfPresent(CrmClueDO::getMobile, reqVO.getMobile())
+                .likeIfPresent(CrmClueDO::getAddress, reqVO.getAddress())
+                .eqIfPresent(CrmClueDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(CrmClueDO::getContactLastTime, reqVO.getContactLastTime())
+                .betweenIfPresent(CrmClueDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmClueDO::getId));
+    }
+
+    default List<CrmClueDO> selectList(CrmClueExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CrmClueDO>()
+                .eqIfPresent(CrmClueDO::getTransformStatus, reqVO.getTransformStatus())
+                .eqIfPresent(CrmClueDO::getFollowUpStatus, reqVO.getFollowUpStatus())
+                .likeIfPresent(CrmClueDO::getName, reqVO.getName())
+                .eqIfPresent(CrmClueDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(CrmClueDO::getContactNextTime, reqVO.getContactNextTime())
+                .likeIfPresent(CrmClueDO::getTelephone, reqVO.getTelephone())
+                .likeIfPresent(CrmClueDO::getMobile, reqVO.getMobile())
+                .likeIfPresent(CrmClueDO::getAddress, reqVO.getAddress())
+                .eqIfPresent(CrmClueDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(CrmClueDO::getContactLastTime, reqVO.getContactLastTime())
+                .betweenIfPresent(CrmClueDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmClueDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java
new file mode 100644
index 000000000..3bf627f5f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueService.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.crm.service.clue;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 线索 Service 接口
+ *
+ * @author Wanwan
+ */
+public interface CrmClueService {
+
+    /**
+     * 创建线索
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createClue(@Valid CrmClueCreateReqVO createReqVO);
+
+    /**
+     * 更新线索
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateClue(@Valid CrmClueUpdateReqVO updateReqVO);
+
+    /**
+     * 删除线索
+     *
+     * @param id 编号
+     */
+    void deleteClue(Long id);
+
+    /**
+     * 获得线索
+     *
+     * @param id 编号
+     * @return 线索
+     */
+    CrmClueDO getClue(Long id);
+
+    /**
+     * 获得线索列表
+     *
+     * @param ids 编号
+     * @return 线索列表
+     */
+    List<CrmClueDO> getClueList(Collection<Long> ids);
+
+    /**
+     * 获得线索分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 线索分页
+     */
+    PageResult<CrmClueDO> getCluePage(CrmCluePageReqVO pageReqVO);
+
+    /**
+     * 获得线索列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 线索列表
+     */
+    List<CrmClueDO> getClueList(CrmClueExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
new file mode 100644
index 000000000..92193361f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.crm.service.clue;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
+import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+
+/**
+ * 线索 Service 实现类
+ *
+ * @author Wanwan
+ */
+@Service
+@Validated
+public class CrmClueServiceImpl implements CrmClueService {
+
+    @Resource
+    private CrmClueMapper clueMapper;
+
+    @Override
+    public Long createClue(CrmClueCreateReqVO createReqVO) {
+        // 插入
+        CrmClueDO clue = CrmClueConvert.INSTANCE.convert(createReqVO);
+        clueMapper.insert(clue);
+        // 返回
+        return clue.getId();
+    }
+
+    @Override
+    public void updateClue(CrmClueUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateClueExists(updateReqVO.getId());
+        // 更新
+        CrmClueDO updateObj = CrmClueConvert.INSTANCE.convert(updateReqVO);
+        clueMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteClue(Long id) {
+        // 校验存在
+        validateClueExists(id);
+        // 删除
+        clueMapper.deleteById(id);
+    }
+
+    private void validateClueExists(Long id) {
+        if (clueMapper.selectById(id) == null) {
+            throw exception(CLUE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmClueDO getClue(Long id) {
+        return clueMapper.selectById(id);
+    }
+
+    @Override
+    public List<CrmClueDO> getClueList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return clueMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CrmClueDO> getCluePage(CrmCluePageReqVO pageReqVO) {
+        return clueMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CrmClueDO> getClueList(CrmClueExportReqVO exportReqVO) {
+        return clueMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
new file mode 100644
index 000000000..3f15cf766
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
new file mode 100644
index 000000000..ce68734c1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
@@ -0,0 +1,231 @@
+package cn.iocoder.yudao.module.crm.service.clue;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link CrmClueServiceImpl} 的单元测试类
+ *
+ * @author Wanwan
+ */
+@Import(CrmClueServiceImpl.class)
+public class CrmClueServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmClueServiceImpl clueService;
+
+    @Resource
+    private CrmClueMapper clueMapper;
+
+    @Test
+    public void testCreateClue_success() {
+        // 准备参数
+        CrmClueCreateReqVO reqVO = randomPojo(CrmClueCreateReqVO.class);
+
+        // 调用
+        Long clueId = clueService.createClue(reqVO);
+        // 断言
+        assertNotNull(clueId);
+        // 校验记录的属性是否正确
+        CrmClueDO clue = clueMapper.selectById(clueId);
+        assertPojoEquals(reqVO, clue);
+    }
+
+    @Test
+    public void testUpdateClue_success() {
+        // mock 数据
+        CrmClueDO dbClue = randomPojo(CrmClueDO.class);
+        clueMapper.insert(dbClue);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmClueUpdateReqVO reqVO = randomPojo(CrmClueUpdateReqVO.class, o -> {
+            o.setId(dbClue.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        clueService.updateClue(reqVO);
+        // 校验是否更新正确
+        CrmClueDO clue = clueMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, clue);
+    }
+
+    @Test
+    public void testUpdateClue_notExists() {
+        // 准备参数
+        CrmClueUpdateReqVO reqVO = randomPojo(CrmClueUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> clueService.updateClue(reqVO), CLUE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteClue_success() {
+        // mock 数据
+        CrmClueDO dbClue = randomPojo(CrmClueDO.class);
+        clueMapper.insert(dbClue);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbClue.getId();
+
+        // 调用
+        clueService.deleteClue(id);
+       // 校验数据不存在了
+       assertNull(clueMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteClue_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> clueService.deleteClue(id), CLUE_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetCluePage() {
+       // mock 数据
+       CrmClueDO dbClue = randomPojo(CrmClueDO.class, o -> { // 等会查询到
+           o.setTransformStatus(null);
+           o.setFollowUpStatus(null);
+           o.setName(null);
+           o.setCustomerId(null);
+           o.setContactNextTime(null);
+           o.setTelephone(null);
+           o.setMobile(null);
+           o.setAddress(null);
+           o.setOwnerUserId(null);
+           o.setContactLastTime(null);
+           o.setCreateTime(null);
+       });
+       clueMapper.insert(dbClue);
+       // 测试 transformStatus 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setTransformStatus(null)));
+       // 测试 followUpStatus 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setFollowUpStatus(null)));
+       // 测试 name 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setName(null)));
+       // 测试 customerId 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCustomerId(null)));
+       // 测试 contactNextTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactNextTime(null)));
+       // 测试 telephone 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setTelephone(null)));
+       // 测试 mobile 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setMobile(null)));
+       // 测试 address 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setAddress(null)));
+       // 测试 ownerUserId 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setOwnerUserId(null)));
+       // 测试 contactLastTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactLastTime(null)));
+       // 测试 createTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmCluePageReqVO reqVO = new CrmCluePageReqVO();
+       reqVO.setTransformStatus(null);
+       reqVO.setFollowUpStatus(null);
+       reqVO.setName(null);
+       reqVO.setCustomerId(null);
+       reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setTelephone(null);
+       reqVO.setMobile(null);
+       reqVO.setAddress(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<CrmClueDO> pageResult = clueService.getCluePage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbClue, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetClueList() {
+       // mock 数据
+       CrmClueDO dbClue = randomPojo(CrmClueDO.class, o -> { // 等会查询到
+           o.setTransformStatus(null);
+           o.setFollowUpStatus(null);
+           o.setName(null);
+           o.setCustomerId(null);
+           o.setContactNextTime(null);
+           o.setTelephone(null);
+           o.setMobile(null);
+           o.setAddress(null);
+           o.setOwnerUserId(null);
+           o.setContactLastTime(null);
+           o.setCreateTime(null);
+       });
+       clueMapper.insert(dbClue);
+       // 测试 transformStatus 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setTransformStatus(null)));
+       // 测试 followUpStatus 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setFollowUpStatus(null)));
+       // 测试 name 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setName(null)));
+       // 测试 customerId 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCustomerId(null)));
+       // 测试 contactNextTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactNextTime(null)));
+       // 测试 telephone 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setTelephone(null)));
+       // 测试 mobile 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setMobile(null)));
+       // 测试 address 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setAddress(null)));
+       // 测试 ownerUserId 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setOwnerUserId(null)));
+       // 测试 contactLastTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactLastTime(null)));
+       // 测试 createTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmClueExportReqVO reqVO = new CrmClueExportReqVO();
+       reqVO.setTransformStatus(null);
+       reqVO.setFollowUpStatus(null);
+       reqVO.setName(null);
+       reqVO.setCustomerId(null);
+       reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setTelephone(null);
+       reqVO.setMobile(null);
+       reqVO.setAddress(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       List<CrmClueDO> list = clueService.getClueList(reqVO);
+       // 断言
+       assertEquals(1, list.size());
+       assertPojoEquals(dbClue, list.get(0));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
index c45d2602e..983e53ab3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
@@ -1 +1,3 @@
 DELETE FROM "crm_contract";
+
+DELETE FROM "crm_clue";
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
index c25a80e44..816264e66 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -24,4 +24,26 @@ CREATE TABLE IF NOT EXISTS "crm_contract" (
   "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   "deleted" bit NOT NULL DEFAULT FALSE,
   PRIMARY KEY ("id")
-    ) COMMENT '合同表';
\ No newline at end of file
+) COMMENT '合同表';
+
+CREATE TABLE IF NOT EXISTS "crm_clue" (
+  "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+  "transform_status" bit NOT NULL,
+  "follow_up_status" bit NOT NULL,
+  "name" varchar NOT NULL,
+  "customer_id" bigint NOT NULL,
+  "contact_next_time" varchar,
+  "telephone" varchar,
+  "mobile" varchar,
+  "address" varchar,
+  "owner_user_id" bigint,
+  "contact_last_time" varchar,
+  "remark" varchar,
+  "creator" varchar DEFAULT '',
+  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "updater" varchar DEFAULT '',
+  "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  "deleted" bit NOT NULL DEFAULT FALSE,
+  "tenant_id" bigint NOT NULL,
+  PRIMARY KEY ("id")
+) COMMENT '线索表';
\ No newline at end of file

From 439edcd1be2c503260a5c0411468c979c175a87f Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 19 Oct 2023 23:15:18 +0800
Subject: [PATCH 010/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E7=BA=BF?=
 =?UTF-8?q?=E7=B4=A2=E7=9A=84=20crud?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |  1 +
 .../admin/clue/CrmClueController.java         | 47 +++++++------------
 .../admin/clue/vo/CrmClueBaseVO.java          | 19 ++++----
 .../admin/clue/vo/CrmCluePageReqVO.java       | 10 ++--
 .../crm/dal/dataobject/clue/CrmClueDO.java    | 20 +++++---
 .../service/clue/CrmClueServiceImplTest.java  | 37 +++++++--------
 6 files changed, 66 insertions(+), 68 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 85eb929fc..3538b16ee 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -12,6 +12,7 @@ public interface ErrorCodeConstants {
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
 
+    // TODO @wanwan:要单独一个分段噢
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_000_001, "线索不存在");
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
index a1979b99a..8892a8e71 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
@@ -1,32 +1,28 @@
 package cn.iocoder.yudao.module.crm.controller.admin.clue;
 
-import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.security.access.prepost.PreAuthorize;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.Operation;
-
-import javax.validation.constraints.*;
-import javax.validation.*;
-import javax.servlet.http.*;
-import java.util.*;
-import java.io.IOException;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
-
 import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.service.clue.CrmClueService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+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
@@ -70,15 +66,6 @@ public class CrmClueController {
         return success(CrmClueConvert.INSTANCE.convert(clue));
     }
 
-    @GetMapping("/list")
-    @Operation(summary = "获得线索列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
-    public CommonResult<List<CrmClueRespVO>> getClueList(@RequestParam("ids") Collection<Long> ids) {
-        List<CrmClueDO> list = clueService.getClueList(ids);
-        return success(CrmClueConvert.INSTANCE.convertList(list));
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得线索分页")
     @PreAuthorize("@ss.hasPermission('crm:clue:query')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
index 285f8029f..9a36c9175 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
@@ -2,15 +2,12 @@ package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
 
 import cn.iocoder.yudao.framework.common.validation.Mobile;
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import javax.validation.constraints.*;
+import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
 /**
@@ -20,18 +17,21 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class CrmClueBaseVO {
 
+    // TODO @wanwan:转化状态,新增和修改的时候,应该不传递的哈;而是在未来的时候,才会更新到
     @Schema(description = "转化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     @NotNull(message = "转化状态不能为空")
     private Boolean transformStatus;
 
+    // TODO @wanwan:同 transformStatus
     @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     @NotNull(message = "跟进状态不能为空")
     private Boolean followUpStatus;
 
     @Schema(description = "线索名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "线索xxx")
-    @NotNull(message = "线索名称不能为空")
+    @NotNull(message = "线索名称不能为空") // TODO @wanwan:应该是 NotEmpty 噢。空串都无法接受
     private String name;
 
+    // TODO @wanwan:中英文之间,要有个空格;例如说,客户 id 不能为空
     @Schema(description = "客户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "520")
     @NotNull(message = "客户id不能为空")
     private Long customerId;
@@ -40,10 +40,12 @@ public class CrmClueBaseVO {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactNextTime;
 
+    // TODO @wanwan:@Schema 在 @Mobile 之前,要保持统一的顺序;2)可以加个 @Telephone 的校验格式;应该不是手机的格式哈
     @Mobile(message = "电话格式不正确")
     @Schema(description = "电话", example = "18000000000")
     private String telephone;
 
+    // TODO @wanwan:@Schema 在 @Mobile 之前,要保持统一的顺序;2)类似 @Mobile 这个提示如果是默认的,就可以不写 message
     @Mobile(message = "手机号格式不正确")
     @Schema(description = "手机号", example = "18000000000")
     private String mobile;
@@ -52,6 +54,7 @@ public class CrmClueBaseVO {
     private String address;
 
     @Schema(description = "负责人的用户编号", example = "27199")
+    // TODO @wanwan:这个是必填字段哈;
     private Long ownerUserId;
 
     @Schema(description = "最后跟进时间")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
index b9cf71c42..f4cba8fed 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
 
-import lombok.*;
-import java.util.*;
-import io.swagger.v3.oas.annotations.media.Schema;
 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;
@@ -15,6 +17,8 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class CrmCluePageReqVO extends PageParam {
 
+    // TODO @wanwan:目前只要支持 name、mobile、telephone 的搜索即可;其它字段应该暂时不需要哈;
+
     @Schema(description = "转化状态", example = "true")
     private Boolean transformStatus;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
index 986d933e8..de30e1301 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
@@ -1,15 +1,15 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.clue;
 
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import com.sun.xml.bind.v2.TODO;
 import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import com.baomidou.mybatisplus.annotation.*;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 
+import java.time.LocalDateTime;
+
+// TODO 芋艿:字段的顺序,需要整理下;
 /**
  * 线索 DO
  *
@@ -68,6 +68,8 @@ public class CrmClueDO extends BaseDO {
     private String address;
     /**
      * 负责人的用户编号
+     *
+     * 关联 AdminUserDO 的 id 字段
      */
     private Long ownerUserId;
     /**
@@ -79,4 +81,8 @@ public class CrmClueDO extends BaseDO {
      */
     private String remark;
 
+    // TODO 芋艿:客户级别;
+    // TODO 芋艿:线索来源;
+    // TODO 芋艿:客户行业;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
index ce68734c1..1051b2fcf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
@@ -1,33 +1,30 @@
 package cn.iocoder.yudao.module.crm.service.clue;
 
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-
-import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
-import org.springframework.context.annotation.Import;
-import java.util.*;
-import java.time.LocalDateTime;
+import java.util.List;
 
-import static cn.hutool.core.util.RandomUtil.*;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
-import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CLUE_NOT_EXISTS;
 import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
 
+// TODO 芋艿:单测后续补;
 /**
  * {@link CrmClueServiceImpl} 的单元测试类
  *

From bee6f41211ce9702aef054e4577d059f9faedd9e Mon Sep 17 00:00:00 2001
From: ljlleo <e0101561@ceic.com>
Date: Fri, 20 Oct 2023 16:01:54 +0800
Subject: [PATCH 011/101] =?UTF-8?q?=E5=95=86=E6=9C=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |   3 +
 .../admin/business/CrmBusinessController.java |  99 ++++++
 .../admin/business/vo/CrmBusinessBaseVO.java  |  78 +++++
 .../business/vo/CrmBusinessCreateReqVO.java   |  14 +
 .../admin/business/vo/CrmBusinessExcelVO.java |  82 +++++
 .../business/vo/CrmBusinessExportReqVO.java   |  74 +++++
 .../business/vo/CrmBusinessPageReqVO.java     |  79 +++++
 .../admin/business/vo/CrmBusinessRespVO.java  |  19 ++
 .../business/vo/CrmBusinessUpdateReqVO.java   |  18 ++
 .../convert/business/CrmBusinessConvert.java  |  34 +++
 .../dataobject/business/CrmBusinessDO.java    | 101 +++++++
 .../dal/mysql/business/CrmBusinessMapper.java |  67 +++++
 .../service/business/CrmBusinessService.java  |  75 +++++
 .../business/CrmBusinessServiceImpl.java      |  90 ++++++
 .../mapper/business/CrmBusinessMapper.xml     |  12 +
 .../business/CrmBusinessServiceImplTest.java  | 283 ++++++++++++++++++
 16 files changed, 1128 insertions(+)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/business/CrmBusinessMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 3538b16ee..66f2abc1f 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -15,4 +15,7 @@ public interface ErrorCodeConstants {
     // TODO @wanwan:要单独一个分段噢
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_000_001, "线索不存在");
 
+    // ========== 商机管理 1-020-001-000 ==========
+    ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_001_000, "商机不存在");
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
new file mode 100644
index 000000000..85cc48f79
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.business.vo.*;
+import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+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/business")
+@Validated
+public class CrmBusinessController {
+
+    @Resource
+    private CrmBusinessService businessService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建商机")
+    @PreAuthorize("@ss.hasPermission('crm:business:create')")
+    public CommonResult<Long> createBusiness(@Valid @RequestBody CrmBusinessCreateReqVO createReqVO) {
+        return success(businessService.createBusiness(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新商机")
+    @PreAuthorize("@ss.hasPermission('crm:business:update')")
+    public CommonResult<Boolean> updateBusiness(@Valid @RequestBody CrmBusinessUpdateReqVO updateReqVO) {
+        businessService.updateBusiness(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除商机")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:business:delete')")
+    public CommonResult<Boolean> deleteBusiness(@RequestParam("id") Long id) {
+        businessService.deleteBusiness(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得商机")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<CrmBusinessRespVO> getBusiness(@RequestParam("id") Long id) {
+        CrmBusinessDO business = businessService.getBusiness(id);
+        return success(CrmBusinessConvert.INSTANCE.convert(business));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得商机列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<List<CrmBusinessRespVO>> getBusinessList(@RequestParam("ids") Collection<Long> ids) {
+        List<CrmBusinessDO> list = businessService.getBusinessList(ids);
+        return success(CrmBusinessConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商机分页")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPage(@Valid CrmBusinessPageReqVO pageVO) {
+        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO);
+        return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出商机 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:business:export')")
+    @OperateLog(type = EXPORT)
+    public void exportBusinessExcel(@Valid CrmBusinessExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmBusinessDO> list = businessService.getBusinessList(exportReqVO);
+        // 导出 Excel
+        List<CrmBusinessExcelVO> datas = CrmBusinessConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "商机.xls", "数据", CrmBusinessExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
new file mode 100644
index 000000000..7e32feea0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 商机 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmBusinessBaseVO {
+
+    @Schema(description = "商机名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotNull(message = "商机名称不能为空")
+    private String name;
+
+    @Schema(description = "商机状态类型编号", example = "25714")
+    @NotNull(message = "商机状态类型不能为空")
+    private Long statusTypeId;
+
+    @Schema(description = "商机状态编号", example = "30320")
+    @NotNull(message = "商机状态不能为空")
+    private Long statusId;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactNextTime;
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10299")
+    @NotNull(message = "客户不能为空")
+    private Long customerId;
+
+    @Schema(description = "预计成交日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime dealTime;
+
+    @Schema(description = "商机金额", example = "12371")
+    private BigDecimal price;
+
+    @Schema(description = "整单折扣")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "产品总金额", example = "12025")
+    private BigDecimal productPrice;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "负责人的用户编号", example = "25562")
+    private Long ownerUserId;
+
+    @Schema(description = "只读权限的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String rwUserIds;
+
+    @Schema(description = "1赢单2输单3无效", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer endStatus;
+
+    @Schema(description = "结束时的备注", example = "你说的对")
+    private String endRemark;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "跟进状态", example = "1")
+    private Integer followUpStatus;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
new file mode 100644
index 000000000..a03540cc6
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 商机创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessCreateReqVO extends CrmBusinessBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java
new file mode 100644
index 000000000..087deafb0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java
@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.math.BigDecimal;
+import java.math.BigDecimal;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * 商机 Excel VO
+ *
+ * @author ljlleo
+ */
+@Data
+public class CrmBusinessExcelVO {
+
+    @ExcelProperty("主键")
+    private Long id;
+
+    @ExcelProperty("商机名称")
+    private String name;
+
+    @ExcelProperty("商机状态类型编号")
+    private Long statusTypeId;
+
+    @ExcelProperty("商机状态编号")
+    private Long statusId;
+
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @ExcelProperty("客户编号")
+    private Long customerId;
+
+    @ExcelProperty("预计成交日期")
+    private LocalDateTime dealTime;
+
+    @ExcelProperty("商机金额")
+    private BigDecimal price;
+
+    @ExcelProperty("整单折扣")
+    private BigDecimal discountPercent;
+
+    @ExcelProperty("产品总金额")
+    private BigDecimal productPrice;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @ExcelProperty("只读权限的用户编号数组")
+    private String roUserIds;
+
+    @ExcelProperty("读写权限的用户编号数组")
+    private String rwUserIds;
+
+    @ExcelProperty("1赢单2输单3无效")
+    private Integer endStatus;
+
+    @ExcelProperty("结束时的备注")
+    private String endRemark;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @ExcelProperty("跟进状态")
+    private Integer followUpStatus;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExportReqVO.java
new file mode 100644
index 000000000..a44283112
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExportReqVO.java
@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 商机 Excel 导出 Request VO,参数和 CrmBusinessPageReqVO 是一致的")
+@Data
+public class CrmBusinessExportReqVO {
+
+    @Schema(description = "商机名称", example = "李四")
+    private String name;
+
+    @Schema(description = "商机状态类型编号", example = "25714")
+    private Long statusTypeId;
+
+    @Schema(description = "商机状态编号", example = "30320")
+    private Long statusId;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactNextTime;
+
+    @Schema(description = "客户编号", example = "10299")
+    private Long customerId;
+
+    @Schema(description = "预计成交日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] dealTime;
+
+    @Schema(description = "商机金额", example = "12371")
+    private BigDecimal price;
+
+    @Schema(description = "整单折扣")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "产品总金额", example = "12025")
+    private BigDecimal productPrice;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "负责人的用户编号", example = "25562")
+    private Long ownerUserId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "只读权限的用户编号数组")
+    private String roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private String rwUserIds;
+
+    @Schema(description = "1赢单2输单3无效", example = "1")
+    private Integer endStatus;
+
+    @Schema(description = "结束时的备注", example = "你说的对")
+    private String endRemark;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactLastTime;
+
+    @Schema(description = "跟进状态", example = "1")
+    private Integer followUpStatus;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
new file mode 100644
index 000000000..636661a9f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
@@ -0,0 +1,79 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.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.math.BigDecimal;
+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 CrmBusinessPageReqVO extends PageParam {
+
+    @Schema(description = "商机名称", example = "李四")
+    private String name;
+
+    @Schema(description = "商机状态类型编号", example = "25714")
+    private Long statusTypeId;
+
+    @Schema(description = "商机状态编号", example = "30320")
+    private Long statusId;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactNextTime;
+
+    @Schema(description = "客户编号", example = "10299")
+    private Long customerId;
+
+    @Schema(description = "预计成交日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] dealTime;
+
+    @Schema(description = "商机金额", example = "12371")
+    private BigDecimal price;
+
+    @Schema(description = "整单折扣")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "产品总金额", example = "12025")
+    private BigDecimal productPrice;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "负责人的用户编号", example = "25562")
+    private Long ownerUserId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "只读权限的用户编号数组")
+    private String roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private String rwUserIds;
+
+    @Schema(description = "1赢单2输单3无效", example = "1")
+    private Integer endStatus;
+
+    @Schema(description = "结束时的备注", example = "你说的对")
+    private String endRemark;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactLastTime;
+
+    @Schema(description = "跟进状态", example = "1")
+    private Integer followUpStatus;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessRespVO.java
new file mode 100644
index 000000000..672f99ec3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 商机 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessRespVO extends CrmBusinessBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
new file mode 100644
index 000000000..8922ec99b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 商机更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessUpdateReqVO extends CrmBusinessBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    @NotNull(message = "主键不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
new file mode 100644
index 000000000..aa03ce0f0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.business;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+
+/**
+ * 商机 Convert
+ *
+ * @author ljlleo
+ */
+@Mapper
+public interface CrmBusinessConvert {
+
+    CrmBusinessConvert INSTANCE = Mappers.getMapper(CrmBusinessConvert.class);
+
+    CrmBusinessDO convert(CrmBusinessCreateReqVO bean);
+
+    CrmBusinessDO convert(CrmBusinessUpdateReqVO bean);
+
+    CrmBusinessRespVO convert(CrmBusinessDO bean);
+
+    List<CrmBusinessRespVO> convertList(List<CrmBusinessDO> list);
+
+    PageResult<CrmBusinessRespVO> convertPage(PageResult<CrmBusinessDO> page);
+
+    List<CrmBusinessExcelVO> convertList02(List<CrmBusinessDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
new file mode 100644
index 000000000..620fb492e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.business;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 商机 DO
+ *
+ * @author ljlleo
+ */
+@TableName("crm_business")
+@KeySequence("crm_business_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmBusinessDO extends BaseDO {
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 商机名称
+     */
+    private String name;
+    /**
+     * 商机状态类型编号
+     */
+    private Long statusTypeId;
+    /**
+     * 商机状态编号
+     */
+    private Long statusId;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime contactNextTime;
+    /**
+     * 客户编号
+     */
+    private Long customerId;
+    /**
+     * 预计成交日期
+     */
+    private LocalDateTime dealTime;
+    /**
+     * 商机金额
+     */
+    private BigDecimal price;
+    /**
+     * 整单折扣
+     */
+    private BigDecimal discountPercent;
+    /**
+     * 产品总金额
+     */
+    private BigDecimal productPrice;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 负责人的用户编号
+     */
+    private Long ownerUserId;
+    /**
+     * 只读权限的用户编号数组
+     */
+    private String roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    private String rwUserIds;
+    /**
+     * 1赢单2输单3无效
+     */
+    private Integer endStatus;
+    /**
+     * 结束时的备注
+     */
+    private String endRemark;
+    /**
+     * 最后跟进时间
+     */
+    private LocalDateTime contactLastTime;
+    /**
+     * 跟进状态
+     */
+    private Integer followUpStatus;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
new file mode 100644
index 000000000..aadec8517
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.business;
+
+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.business.vo.CrmBusinessExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 商机 Mapper
+ *
+ * @author ljlleo
+ */
+@Mapper
+public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
+
+    default PageResult<CrmBusinessDO> selectPage(CrmBusinessPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmBusinessDO>()
+                .likeIfPresent(CrmBusinessDO::getName, reqVO.getName())
+                .eqIfPresent(CrmBusinessDO::getStatusTypeId, reqVO.getStatusTypeId())
+                .eqIfPresent(CrmBusinessDO::getStatusId, reqVO.getStatusId())
+                .betweenIfPresent(CrmBusinessDO::getContactNextTime, reqVO.getContactNextTime())
+                .eqIfPresent(CrmBusinessDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(CrmBusinessDO::getDealTime, reqVO.getDealTime())
+                .eqIfPresent(CrmBusinessDO::getPrice, reqVO.getPrice())
+                .eqIfPresent(CrmBusinessDO::getDiscountPercent, reqVO.getDiscountPercent())
+                .eqIfPresent(CrmBusinessDO::getProductPrice, reqVO.getProductPrice())
+                .eqIfPresent(CrmBusinessDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(CrmBusinessDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(CrmBusinessDO::getCreateTime, reqVO.getCreateTime())
+                .eqIfPresent(CrmBusinessDO::getRoUserIds, reqVO.getRoUserIds())
+                .eqIfPresent(CrmBusinessDO::getRwUserIds, reqVO.getRwUserIds())
+                .eqIfPresent(CrmBusinessDO::getEndStatus, reqVO.getEndStatus())
+                .eqIfPresent(CrmBusinessDO::getEndRemark, reqVO.getEndRemark())
+                .betweenIfPresent(CrmBusinessDO::getContactLastTime, reqVO.getContactLastTime())
+                .eqIfPresent(CrmBusinessDO::getFollowUpStatus, reqVO.getFollowUpStatus())
+                .orderByDesc(CrmBusinessDO::getId));
+    }
+
+    default List<CrmBusinessDO> selectList(CrmBusinessExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
+                .likeIfPresent(CrmBusinessDO::getName, reqVO.getName())
+                .eqIfPresent(CrmBusinessDO::getStatusTypeId, reqVO.getStatusTypeId())
+                .eqIfPresent(CrmBusinessDO::getStatusId, reqVO.getStatusId())
+                .betweenIfPresent(CrmBusinessDO::getContactNextTime, reqVO.getContactNextTime())
+                .eqIfPresent(CrmBusinessDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(CrmBusinessDO::getDealTime, reqVO.getDealTime())
+                .eqIfPresent(CrmBusinessDO::getPrice, reqVO.getPrice())
+                .eqIfPresent(CrmBusinessDO::getDiscountPercent, reqVO.getDiscountPercent())
+                .eqIfPresent(CrmBusinessDO::getProductPrice, reqVO.getProductPrice())
+                .eqIfPresent(CrmBusinessDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(CrmBusinessDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(CrmBusinessDO::getCreateTime, reqVO.getCreateTime())
+                .eqIfPresent(CrmBusinessDO::getRoUserIds, reqVO.getRoUserIds())
+                .eqIfPresent(CrmBusinessDO::getRwUserIds, reqVO.getRwUserIds())
+                .eqIfPresent(CrmBusinessDO::getEndStatus, reqVO.getEndStatus())
+                .eqIfPresent(CrmBusinessDO::getEndRemark, reqVO.getEndRemark())
+                .betweenIfPresent(CrmBusinessDO::getContactLastTime, reqVO.getContactLastTime())
+                .eqIfPresent(CrmBusinessDO::getFollowUpStatus, reqVO.getFollowUpStatus())
+                .orderByDesc(CrmBusinessDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
new file mode 100644
index 000000000..4bb352c58
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.crm.service.business;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 商机 Service 接口
+ *
+ * @author ljlleo
+ */
+public interface CrmBusinessService {
+
+    /**
+     * 创建商机
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createBusiness(@Valid CrmBusinessCreateReqVO createReqVO);
+
+    /**
+     * 更新商机
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateBusiness(@Valid CrmBusinessUpdateReqVO updateReqVO);
+
+    /**
+     * 删除商机
+     *
+     * @param id 编号
+     */
+    void deleteBusiness(Long id);
+
+    /**
+     * 获得商机
+     *
+     * @param id 编号
+     * @return 商机
+     */
+    CrmBusinessDO getBusiness(Long id);
+
+    /**
+     * 获得商机列表
+     *
+     * @param ids 编号
+     * @return 商机列表
+     */
+    List<CrmBusinessDO> getBusinessList(Collection<Long> ids);
+
+    /**
+     * 获得商机分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 商机分页
+     */
+    PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO);
+
+    /**
+     * 获得商机列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 商机列表
+     */
+    List<CrmBusinessDO> getBusinessList(CrmBusinessExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
new file mode 100644
index 000000000..44fa74f80
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -0,0 +1,90 @@
+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.module.crm.controller.admin.business.vo.CrmBusinessCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
+
+/**
+ * 商机 Service 实现类
+ *
+ * @author ljlleo
+ */
+@Service
+@Validated
+public class CrmBusinessServiceImpl implements CrmBusinessService {
+
+    @Resource
+    private CrmBusinessMapper businessMapper;
+
+    @Override
+    public Long createBusiness(CrmBusinessCreateReqVO createReqVO) {
+        // 插入
+        CrmBusinessDO business = CrmBusinessConvert.INSTANCE.convert(createReqVO);
+        businessMapper.insert(business);
+        // 返回
+        return business.getId();
+    }
+
+    @Override
+    public void updateBusiness(CrmBusinessUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateBusinessExists(updateReqVO.getId());
+        // 更新
+        CrmBusinessDO updateObj = CrmBusinessConvert.INSTANCE.convert(updateReqVO);
+        businessMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteBusiness(Long id) {
+        // 校验存在
+        validateBusinessExists(id);
+        // 删除
+        businessMapper.deleteById(id);
+    }
+
+    private void validateBusinessExists(Long id) {
+        if (businessMapper.selectById(id) == null) {
+            throw exception(BUSINESS_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmBusinessDO getBusiness(Long id) {
+        return businessMapper.selectById(id);
+    }
+
+    @Override
+    public List<CrmBusinessDO> getBusinessList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return businessMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO) {
+        return businessMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CrmBusinessDO> getBusinessList(CrmBusinessExportReqVO exportReqVO) {
+        return businessMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/business/CrmBusinessMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/business/CrmBusinessMapper.xml
new file mode 100644
index 000000000..c35aafec7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/business/CrmBusinessMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
new file mode 100644
index 000000000..64d3f1e71
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
@@ -0,0 +1,283 @@
+package cn.iocoder.yudao.module.crm.service.business;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link CrmBusinessServiceImpl} 的单元测试类
+ *
+ * @author ljlleo
+ */
+@Import(CrmBusinessServiceImpl.class)
+public class CrmBusinessServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmBusinessServiceImpl businessService;
+
+    @Resource
+    private CrmBusinessMapper businessMapper;
+
+    @Test
+    public void testCreateBusiness_success() {
+        // 准备参数
+        CrmBusinessCreateReqVO reqVO = randomPojo(CrmBusinessCreateReqVO.class);
+
+        // 调用
+        Long businessId = businessService.createBusiness(reqVO);
+        // 断言
+        assertNotNull(businessId);
+        // 校验记录的属性是否正确
+        CrmBusinessDO business = businessMapper.selectById(businessId);
+        assertPojoEquals(reqVO, business);
+    }
+
+    @Test
+    public void testUpdateBusiness_success() {
+        // mock 数据
+        CrmBusinessDO dbBusiness = randomPojo(CrmBusinessDO.class);
+        businessMapper.insert(dbBusiness);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmBusinessUpdateReqVO reqVO = randomPojo(CrmBusinessUpdateReqVO.class, o -> {
+            o.setId(dbBusiness.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        businessService.updateBusiness(reqVO);
+        // 校验是否更新正确
+        CrmBusinessDO business = businessMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, business);
+    }
+
+    @Test
+    public void testUpdateBusiness_notExists() {
+        // 准备参数
+        CrmBusinessUpdateReqVO reqVO = randomPojo(CrmBusinessUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> businessService.updateBusiness(reqVO), BUSINESS_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteBusiness_success() {
+        // mock 数据
+        CrmBusinessDO dbBusiness = randomPojo(CrmBusinessDO.class);
+        businessMapper.insert(dbBusiness);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbBusiness.getId();
+
+        // 调用
+        businessService.deleteBusiness(id);
+       // 校验数据不存在了
+       assertNull(businessMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteBusiness_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> businessService.deleteBusiness(id), BUSINESS_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetBusinessPage() {
+       // mock 数据
+       CrmBusinessDO dbBusiness = randomPojo(CrmBusinessDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setStatusTypeId(null);
+           o.setStatusId(null);
+           o.setContactNextTime(null);
+           o.setCustomerId(null);
+           o.setDealTime(null);
+           o.setPrice(null);
+           o.setDiscountPercent(null);
+           o.setProductPrice(null);
+           o.setRemark(null);
+           o.setOwnerUserId(null);
+           o.setCreateTime(null);
+           o.setRoUserIds(null);
+           o.setRwUserIds(null);
+           o.setEndStatus(null);
+           o.setEndRemark(null);
+           o.setContactLastTime(null);
+           o.setFollowUpStatus(null);
+       });
+       businessMapper.insert(dbBusiness);
+       // 测试 name 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setName(null)));
+       // 测试 statusTypeId 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setStatusTypeId(null)));
+       // 测试 statusId 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setStatusId(null)));
+       // 测试 contactNextTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setContactNextTime(null)));
+       // 测试 customerId 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setCustomerId(null)));
+       // 测试 dealTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setDealTime(null)));
+       // 测试 price 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setPrice(null)));
+       // 测试 discountPercent 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setDiscountPercent(null)));
+       // 测试 productPrice 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setProductPrice(null)));
+       // 测试 remark 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRemark(null)));
+       // 测试 ownerUserId 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setOwnerUserId(null)));
+       // 测试 createTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setCreateTime(null)));
+       // 测试 roUserIds 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRoUserIds(null)));
+       // 测试 rwUserIds 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRwUserIds(null)));
+       // 测试 endStatus 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setEndStatus(null)));
+       // 测试 endRemark 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setEndRemark(null)));
+       // 测试 contactLastTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setContactLastTime(null)));
+       // 测试 followUpStatus 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setFollowUpStatus(null)));
+       // 准备参数
+       CrmBusinessPageReqVO reqVO = new CrmBusinessPageReqVO();
+       reqVO.setName(null);
+       reqVO.setStatusTypeId(null);
+       reqVO.setStatusId(null);
+       reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCustomerId(null);
+       reqVO.setDealTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setPrice(null);
+       reqVO.setDiscountPercent(null);
+       reqVO.setProductPrice(null);
+       reqVO.setRemark(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setRoUserIds(null);
+       reqVO.setRwUserIds(null);
+       reqVO.setEndStatus(null);
+       reqVO.setEndRemark(null);
+       reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setFollowUpStatus(null);
+
+       // 调用
+       PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbBusiness, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetBusinessList() {
+       // mock 数据
+       CrmBusinessDO dbBusiness = randomPojo(CrmBusinessDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setStatusTypeId(null);
+           o.setStatusId(null);
+           o.setContactNextTime(null);
+           o.setCustomerId(null);
+           o.setDealTime(null);
+           o.setPrice(null);
+           o.setDiscountPercent(null);
+           o.setProductPrice(null);
+           o.setRemark(null);
+           o.setOwnerUserId(null);
+           o.setCreateTime(null);
+           o.setRoUserIds(null);
+           o.setRwUserIds(null);
+           o.setEndStatus(null);
+           o.setEndRemark(null);
+           o.setContactLastTime(null);
+           o.setFollowUpStatus(null);
+       });
+       businessMapper.insert(dbBusiness);
+       // 测试 name 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setName(null)));
+       // 测试 statusTypeId 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setStatusTypeId(null)));
+       // 测试 statusId 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setStatusId(null)));
+       // 测试 contactNextTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setContactNextTime(null)));
+       // 测试 customerId 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setCustomerId(null)));
+       // 测试 dealTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setDealTime(null)));
+       // 测试 price 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setPrice(null)));
+       // 测试 discountPercent 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setDiscountPercent(null)));
+       // 测试 productPrice 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setProductPrice(null)));
+       // 测试 remark 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRemark(null)));
+       // 测试 ownerUserId 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setOwnerUserId(null)));
+       // 测试 createTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setCreateTime(null)));
+       // 测试 roUserIds 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRoUserIds(null)));
+       // 测试 rwUserIds 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRwUserIds(null)));
+       // 测试 endStatus 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setEndStatus(null)));
+       // 测试 endRemark 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setEndRemark(null)));
+       // 测试 contactLastTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setContactLastTime(null)));
+       // 测试 followUpStatus 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setFollowUpStatus(null)));
+       // 准备参数
+       CrmBusinessExportReqVO reqVO = new CrmBusinessExportReqVO();
+       reqVO.setName(null);
+       reqVO.setStatusTypeId(null);
+       reqVO.setStatusId(null);
+       reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCustomerId(null);
+       reqVO.setDealTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setPrice(null);
+       reqVO.setDiscountPercent(null);
+       reqVO.setProductPrice(null);
+       reqVO.setRemark(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setRoUserIds(null);
+       reqVO.setRwUserIds(null);
+       reqVO.setEndStatus(null);
+       reqVO.setEndRemark(null);
+       reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setFollowUpStatus(null);
+
+       // 调用
+       List<CrmBusinessDO> list = businessService.getBusinessList(reqVO);
+       // 断言
+       assertEquals(1, list.size());
+       assertPojoEquals(dbBusiness, list.get(0));
+    }
+
+}

From 01490c879c822f5732c5b9dc55831a002ee10f11 Mon Sep 17 00:00:00 2001
From: ljlleo <e0101561@ceic.com>
Date: Fri, 20 Oct 2023 16:14:45 +0800
Subject: [PATCH 012/101] =?UTF-8?q?=E5=95=86=E6=9C=BAsql?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql      | 31 +++++++++++++++++++++-
 sql/mysql/crm_menu.sql | 60 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 90 insertions(+), 1 deletion(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 6c5594068..24cc3c623 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -60,4 +60,33 @@ CREATE TABLE `crm_clue`  (
      `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
      `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
      PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
\ No newline at end of file
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
+
+DROP TABLE IF EXISTS `crm_business`;
+CREATE TABLE `crm_business` (
+                                `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                                `name` varchar(100) NOT NULL COMMENT '商机名称',
+                                `status_type_id` bigint DEFAULT NULL COMMENT '商机状态类型编号',
+                                `status_id` bigint DEFAULT NULL COMMENT '商机状态编号',
+                                `contact_next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
+                                `customer_id` bigint NOT NULL COMMENT '客户编号',
+                                `deal_time` datetime DEFAULT NULL COMMENT '预计成交日期',
+                                `price` decimal(18,2) DEFAULT NULL COMMENT '商机金额',
+                                `discount_percent` decimal(10,2) DEFAULT NULL COMMENT '整单折扣',
+                                `product_price` decimal(18,2) DEFAULT NULL COMMENT '产品总金额',
+                                `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+                                `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '创建人',
+                                `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '更新人',
+                                `owner_user_id` bigint DEFAULT NULL COMMENT '负责人的用户编号',
+                                `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+                                `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
+                                `ro_user_ids` longtext NOT NULL COMMENT '只读权限的用户编号数组',
+                                `rw_user_ids` longtext NOT NULL COMMENT '读写权限的用户编号数组',
+                                `end_status` int NOT NULL COMMENT '1赢单2输单3无效',
+                                `end_remark` varchar(500) DEFAULT NULL COMMENT '结束时的备注',
+                                `deleted` bit(1) DEFAULT b'0' COMMENT '逻辑删除',
+                                `contact_last_time` datetime DEFAULT NULL COMMENT '最后跟进时间',
+                                `follow_up_status` int DEFAULT NULL COMMENT '跟进状态',
+                                `tenant_id` bigint DEFAULT '0' COMMENT '租户ID',
+                                PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商机表';
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index cf0fe00fd..58c2fd194 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -117,3 +117,63 @@ VALUES (
    '线索导出', 'crm:clue:export', 3, 5, @parentId,
    '', '', '', 0
 );
+
+-- ----------------------------
+-- 合同菜单
+-- ----------------------------
+
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+           '商机管理', '', 2, 0, '',
+           'business', '', 'crm/business/index', 0, 'CrmBusiness'
+       );
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '商机查询', 'crm:business:query', 3, 1, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '商机创建', 'crm:business:create', 3, 2, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '商机更新', 'crm:business:update', 3, 3, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '商机删除', 'crm:business:delete', 3, 4, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '商机导出', 'crm:business:export', 3, 5, @parentId,
+           '', '', '', 0
+       );

From 73f4fc8ceb4adbca1a9b9c75f87f684b7a98aaa6 Mon Sep 17 00:00:00 2001
From: liuhongfeng <291117974@qq.com>
Date: Fri, 20 Oct 2023 22:16:34 +0800
Subject: [PATCH 013/101] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91?=
 =?UTF-8?q?=E5=9B=9E=E6=AC=BE=E7=AE=A1=E7=90=86=20=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  34 ++-
 sql/mysql/crm_data.sql                        |  19 ++
 sql/mysql/crm_menu.sql                        |  69 ++++-
 .../module/crm/enums/ErrorCodeConstants.java  |   2 +
 .../receivable/ReceivableController.java      | 101 +++++++
 .../admin/receivable/vo/ReceivableBaseVO.java |  72 +++++
 .../receivable/vo/ReceivableCreateReqVO.java  |  14 +
 .../receivable/vo/ReceivableExcelVO.java      |  80 +++++
 .../receivable/vo/ReceivableExportReqVO.java  |  72 +++++
 .../receivable/vo/ReceivablePageReqVO.java    |  73 +++++
 .../admin/receivable/vo/ReceivableRespVO.java |  19 ++
 .../receivable/vo/ReceivableUpdateReqVO.java  |  18 ++
 .../convert/receivable/ReceivableConvert.java |  34 +++
 .../dataobject/receivable/ReceivableDO.java   | 101 +++++++
 .../mysql/receivable/ReceivableMapper.java    |  64 ++++
 .../service/receivable/ReceivableService.java |  71 +++++
 .../receivable/ReceivableServiceImpl.java     |  87 ++++++
 .../mapper/receivable/ReceivableMapper.xml    |  12 +
 .../receivable/ReceivableServiceImplTest.java | 279 ++++++++++++++++++
 .../src/test/resources/sql/clean.sql          |   4 +-
 .../src/test/resources/sql/create_tables.sql  |  28 +-
 21 files changed, 1249 insertions(+), 4 deletions(-)
 create mode 100644 sql/mysql/crm_data.sql
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivableConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivableMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 6c5594068..699f69307 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -60,4 +60,36 @@ CREATE TABLE `crm_clue`  (
      `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
      `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
      PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
\ No newline at end of file
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
+
+
+-- ----------------------------
+-- 回款表
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_receivable`;
+CREATE TABLE `crm_receivable`  (
+   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+   `no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '回款编号',
+   `plan_id` bigint(20) NULL DEFAULT NULL COMMENT '回款计划ID',
+   `customer_id` bigint(20) NULL DEFAULT NULL COMMENT '客户ID',
+   `contract_id` bigint(20) NULL DEFAULT NULL COMMENT '合同ID',
+   `check_status` tinyint(4) NULL DEFAULT NULL COMMENT '审批状态',
+   `process_instance_id` bigint(20) NULL DEFAULT NULL COMMENT '工作流编号',
+   `return_time` datetime NULL DEFAULT NULL COMMENT '回款日期',
+   `return_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '回款方式',
+   `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '回款金额',
+   `owner_user_id` bigint(20) NULL DEFAULT NULL COMMENT '负责人的用户编号',
+   `batch_id` bigint(20) NULL DEFAULT NULL COMMENT '批次',
+   `sort` int(11) NULL DEFAULT NULL COMMENT '显示顺序',
+   `data_scope` tinyint(4) NULL DEFAULT 1 COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
+   `data_scope_dept_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '数据范围(指定部门数组)',
+   `status` tinyint(4) NOT NULL COMMENT '状态(0正常 1停用)',
+   `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
+   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+   `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
+   PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回款管理' ROW_FORMAT = DYNAMIC;
diff --git a/sql/mysql/crm_data.sql b/sql/mysql/crm_data.sql
new file mode 100644
index 000000000..dfc1135f9
--- /dev/null
+++ b/sql/mysql/crm_data.sql
@@ -0,0 +1,19 @@
+
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (184, '回款管理审批状态', 'crm_receivable_check_status', 0, '回款管理审批状态(0 未审核 1 审核通过 2 审核拒绝 3 审核中 4 已撤回)', '1', '2023-10-18 21:44:24', '1', '2023-10-18 21:44:24', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (185, '回款管理-回款方式', 'crm_return_type', 0, '回款管理-回款方式', '1', '2023-10-18 21:54:10', '1', '2023-10-18 21:54:10', b'0', '1970-01-01 00:00:00');
+
+
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1389, 0, '未审核', '0', 'crm_receivable_check_status', 0, 'default', '', '0 未审核 ', '1', '2023-10-18 21:46:00', '1', '2023-10-18 21:47:16', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1390, 1, '审核通过', '1', 'crm_receivable_check_status', 0, 'default', '', '1 审核通过', '1', '2023-10-18 21:46:18', '1', '2023-10-18 21:47:08', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1391, 2, '审核拒绝', '2', 'crm_receivable_check_status', 0, 'default', '', ' 2 审核拒绝', '1', '2023-10-18 21:46:58', '1', '2023-10-18 21:47:21', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1392, 3, '审核中', '3', 'crm_receivable_check_status', 0, 'default', '', ' 3 审核中', '1', '2023-10-18 21:47:35', '1', '2023-10-18 21:47:35', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1393, 4, '已撤回', '4', 'crm_receivable_check_status', 0, 'default', '', ' 4 已撤回', '1', '2023-10-18 21:47:46', '1', '2023-10-18 21:47:46', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1394, 1, '支票', '1', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:29', '1', '2023-10-18 21:54:29', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1395, 2, '现金', '2', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:41', '1', '2023-10-18 21:54:41', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1396, 3, '邮政汇款', '3', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:53', '1', '2023-10-18 21:54:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1397, 4, '电汇', '4', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:07', '1', '2023-10-18 21:55:07', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1398, 5, '网上转账', '5', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:24', '1', '2023-10-18 21:55:24', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1399, 6, '支付宝', '6', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:38', '1', '2023-10-18 21:55:38', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1400, 7, '微信支付', '7', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:53', '1', '2023-10-18 21:55:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1401, 8, '其他', '8', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:56:06', '1', '2023-10-18 21:56:06', b'0');
+
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index cf0fe00fd..25c07c6a8 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -8,7 +8,7 @@ INSERT INTO system_menu(
     path, icon, component, status, component_name
 )
 VALUES (
-    '合同管理', '', 2, 0, 1254,
+    '合同管理', '', 2, 0, 2375,
     'contract', '', 'crm/contract/index', 0, 'Contract'
 );
 
@@ -117,3 +117,70 @@ VALUES (
    '线索导出', 'crm:clue:export', 3, 5, @parentId,
    '', '', '', 0
 );
+
+
+-- ----------------------------
+-- 客户管理菜单
+-- ----------------------------
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2375, '客户管理', '', 1, 0, 0, '/crm', 'ep:avatar', '', '', 0, b'1', b'1', b'1', '1', '2023-10-20 00:36:13', '1', '2023-10-19 16:37:24', b'0');
+
+
+-- ----------------------------
+-- 回款菜单
+-- ----------------------------
+
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+           '回款管理', '', 2, 0, 2375,
+           'receivable', '', 'crm/receivable/index', 0, 'Receivable'
+       );
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款管理查询', 'crm:receivable:query', 3, 1, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款管理创建', 'crm:receivable:create', 3, 2, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款管理更新', 'crm:receivable:update', 3, 3, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款管理删除', 'crm:receivable:delete', 3, 4, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款管理导出', 'crm:receivable:export', 3, 5, @parentId,
+           '', '', '', 0
+       );
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 3538b16ee..6c0d03d93 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -15,4 +15,6 @@ public interface ErrorCodeConstants {
     // TODO @wanwan:要单独一个分段噢
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_000_001, "线索不存在");
 
+    ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_030_000_001, "回款管理不存在");
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
new file mode 100644
index 000000000..94c3a1c70
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.*;
+import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+import cn.iocoder.yudao.module.crm.service.receivable.ReceivableService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+/**
+ * @author 赤焰
+ */
+@Tag(name = "管理后台 - 回款管理")
+@RestController
+@RequestMapping("/crm/receivable")
+@Validated
+public class ReceivableController {
+
+    @Resource
+    private ReceivableService receivableService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建回款管理")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:create')")
+    public CommonResult<Long> createReceivable(@Valid @RequestBody ReceivableCreateReqVO createReqVO) {
+        return success(receivableService.createReceivable(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新回款管理")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:update')")
+    public CommonResult<Boolean> updateReceivable(@Valid @RequestBody ReceivableUpdateReqVO updateReqVO) {
+        receivableService.updateReceivable(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除回款管理")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:receivable:delete')")
+    public CommonResult<Boolean> deleteReceivable(@RequestParam("id") Long id) {
+        receivableService.deleteReceivable(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得回款管理")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
+    public CommonResult<ReceivableRespVO> getReceivable(@RequestParam("id") Long id) {
+        ReceivableDO receivable = receivableService.getReceivable(id);
+        return success(ReceivableConvert.INSTANCE.convert(receivable));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得回款管理列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
+    public CommonResult<List<ReceivableRespVO>> getReceivableList(@RequestParam("ids") Collection<Long> ids) {
+        List<ReceivableDO> list = receivableService.getReceivableList(ids);
+        return success(ReceivableConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得回款管理分页")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
+    public CommonResult<PageResult<ReceivableRespVO>> getReceivablePage(@Valid ReceivablePageReqVO pageVO) {
+        PageResult<ReceivableDO> pageResult = receivableService.getReceivablePage(pageVO);
+        return success(ReceivableConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出回款管理 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:export')")
+    @OperateLog(type = EXPORT)
+    public void exportReceivableExcel(@Valid ReceivableExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<ReceivableDO> list = receivableService.getReceivableList(exportReqVO);
+        // 导出 Excel
+        List<ReceivableExcelVO> datas = ReceivableConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "回款管理.xls", "数据", ReceivableExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
new file mode 100644
index 000000000..4b63f1466
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import javax.validation.constraints.*;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 回款管理 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ReceivableBaseVO {
+
+    @Schema(description = "回款编号")
+    private String no;
+
+    @Schema(description = "回款计划ID", example = "31177")
+    private Long planId;
+
+    @Schema(description = "客户ID", example = "4963")
+    private Long customerId;
+
+    @Schema(description = "合同ID", example = "30305")
+    private Long contractId;
+
+    @Schema(description = "审批状态", example = "1")
+    private Integer checkStatus;
+
+    @Schema(description = "工作流编号", example = "16568")
+    private Long processInstanceId;
+
+    @Schema(description = "回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime returnTime;
+
+    @Schema(description = "回款方式", example = "2")
+    private String returnType;
+
+    @Schema(description = "回款金额", example = "31859")
+    private BigDecimal price;
+
+    @Schema(description = "负责人", example = "22202")
+    private Long ownerUserId;
+
+    @Schema(description = "批次", example = "2539")
+    private Long batchId;
+
+    @Schema(description = "显示顺序")
+    private Integer sort;
+
+    @Schema(description = "数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
+    private Integer dataScope;
+
+    @Schema(description = "数据范围(指定部门数组)")
+    private String dataScopeDeptIds;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "状态不能为空")
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java
new file mode 100644
index 000000000..5bbb4fce2
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 回款管理创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ReceivableCreateReqVO extends ReceivableBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
new file mode 100644
index 000000000..f60330c53
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+
+/**
+ * 回款管理 Excel VO
+ *
+ * @author 赤焰
+ */
+@Data
+public class ReceivableExcelVO {
+
+    @ExcelProperty("ID")
+    private Long id;
+
+    @ExcelProperty("回款编号")
+    private String no;
+
+    @ExcelProperty("回款计划ID")
+    private Long planId;
+
+    @ExcelProperty("客户ID")
+    private Long customerId;
+
+    @ExcelProperty("合同ID")
+    private Long contractId;
+
+    @ExcelProperty(value = "审批状态", converter = DictConvert.class)
+    @DictFormat("crm_receivable_check_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Integer checkStatus;
+
+    @ExcelProperty("工作流编号")
+    private Long processInstanceId;
+
+    @ExcelProperty("回款日期")
+    private LocalDateTime returnTime;
+
+    @ExcelProperty("回款方式")
+    private String returnType;
+
+    @ExcelProperty("回款金额")
+    private BigDecimal price;
+
+    @ExcelProperty("负责人")
+    private Long ownerUserId;
+
+    @ExcelProperty("批次")
+    private Long batchId;
+
+    //@ExcelProperty("显示顺序")
+    //private Integer sort;
+
+    //@ExcelProperty("数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
+    //private Integer dataScope;
+
+    //@ExcelProperty("数据范围(指定部门数组)")
+    //private String dataScopeDeptIds;
+
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat("common_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Integer status;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
new file mode 100644
index 000000000..7ea31a0fb
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * @author 赤焰
+ */
+@Schema(description = "管理后台 - 回款管理 Excel 导出 Request VO,参数和 ReceivablePageReqVO 是一致的")
+@Data
+public class ReceivableExportReqVO {
+
+    @Schema(description = "回款编号")
+    private String no;
+
+    @Schema(description = "回款计划ID", example = "31177")
+    private Long planId;
+
+    @Schema(description = "客户ID", example = "4963")
+    private Long customerId;
+
+    @Schema(description = "合同ID", example = "30305")
+    private Long contractId;
+
+    @Schema(description = "审批状态", example = "1")
+    private Integer checkStatus;
+
+    @Schema(description = "工作流编号", example = "16568")
+    private Long processInstanceId;
+
+    @Schema(description = "回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] returnTime;
+
+    @Schema(description = "回款方式", example = "2")
+    private String returnType;
+
+    @Schema(description = "回款金额", example = "31859")
+    private BigDecimal price;
+
+    @Schema(description = "负责人", example = "22202")
+    private Long ownerUserId;
+
+    @Schema(description = "批次", example = "2539")
+    private Long batchId;
+
+    @Schema(description = "显示顺序")
+    private Integer sort;
+
+    @Schema(description = "数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
+    private Integer dataScope;
+
+    @Schema(description = "数据范围(指定部门数组)")
+    private String dataScopeDeptIds;
+
+    @Schema(description = "状态", example = "1")
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
new file mode 100644
index 000000000..7b9e9df6d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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 ReceivablePageReqVO extends PageParam {
+
+    @Schema(description = "回款编号")
+    private String no;
+
+    @Schema(description = "回款计划ID", example = "31177")
+    private Long planId;
+
+    @Schema(description = "客户ID", example = "4963")
+    private Long customerId;
+
+    @Schema(description = "合同ID", example = "30305")
+    private Long contractId;
+
+    @Schema(description = "审批状态", example = "1")
+    private Integer checkStatus;
+
+    @Schema(description = "工作流编号", example = "16568")
+    private Long processInstanceId;
+
+    @Schema(description = "回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] returnTime;
+
+    @Schema(description = "回款方式", example = "2")
+    private String returnType;
+
+    @Schema(description = "回款金额", example = "31859")
+    private BigDecimal price;
+
+    @Schema(description = "负责人", example = "22202")
+    private Long ownerUserId;
+
+    @Schema(description = "批次", example = "2539")
+    private Long batchId;
+
+    @Schema(description = "显示顺序")
+    private Integer sort;
+
+    @Schema(description = "数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
+    private Integer dataScope;
+
+    @Schema(description = "数据范围(指定部门数组)")
+    private String dataScopeDeptIds;
+
+    @Schema(description = "状态", example = "1")
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java
new file mode 100644
index 000000000..fd26e312e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 回款管理 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ReceivableRespVO extends ReceivableBaseVO {
+
+    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java
new file mode 100644
index 000000000..600b09943
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 回款管理更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ReceivableUpdateReqVO extends ReceivableBaseVO {
+
+    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
+    @NotNull(message = "ID不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivableConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivableConvert.java
new file mode 100644
index 000000000..7f14aaddd
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivableConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.receivable;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+
+/**
+ * 回款管理 Convert
+ *
+ * @author 赤焰
+ */
+@Mapper
+public interface ReceivableConvert {
+
+    ReceivableConvert INSTANCE = Mappers.getMapper(ReceivableConvert.class);
+
+    ReceivableDO convert(ReceivableCreateReqVO bean);
+
+    ReceivableDO convert(ReceivableUpdateReqVO bean);
+
+    ReceivableRespVO convert(ReceivableDO bean);
+
+    List<ReceivableRespVO> convertList(List<ReceivableDO> list);
+
+    PageResult<ReceivableRespVO> convertPage(PageResult<ReceivableDO> page);
+
+    List<ReceivableExcelVO> convertList02(List<ReceivableDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
new file mode 100644
index 000000000..561765b09
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 回款管理 DO
+ *
+ * @author 赤焰
+ */
+@TableName("crm_receivable")
+@KeySequence("crm_receivable_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReceivableDO extends BaseDO {
+
+    /**
+     * ID
+     */
+    @TableId
+    private Long id;
+    /**
+     * 回款编号
+     */
+    private String no;
+    /**
+     * 回款计划ID
+     */
+    private Long planId;
+    /**
+     * 客户ID
+     */
+    private Long customerId;
+    /**
+     * 合同ID
+     */
+    private Long contractId;
+    /**
+     * 审批状态
+     *
+     * 枚举 {@link TODO crm_receivable_check_status 对应的类}
+     */
+    private Integer checkStatus;
+    /**
+     * 工作流编号
+     */
+    private Long processInstanceId;
+    /**
+     * 回款日期
+     */
+    private LocalDateTime returnTime;
+    /**
+     * 回款方式
+     */
+    private String returnType;
+    /**
+     * 回款金额
+     */
+    private BigDecimal price;
+    /**
+     * 负责人
+     */
+    private Long ownerUserId;
+    /**
+     * 批次
+     */
+    private Long batchId;
+    /**
+     * 显示顺序
+     */
+    private Integer sort;
+    /**
+     * 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)
+     */
+    private Integer dataScope;
+    /**
+     * 数据范围(指定部门数组)
+     */
+    private String dataScopeDeptIds;
+    /**
+     * 状态
+     *
+     * 枚举 {@link TODO common_status 对应的类}
+     */
+    private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
new file mode 100644
index 000000000..08031772c
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+
+/**
+ * 回款管理 Mapper
+ *
+ * @author 赤焰
+ */
+@Mapper
+public interface ReceivableMapper extends BaseMapperX<ReceivableDO> {
+
+    default PageResult<ReceivableDO> selectPage(ReceivablePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ReceivableDO>()
+                .eqIfPresent(ReceivableDO::getNo, reqVO.getNo())
+                .eqIfPresent(ReceivableDO::getPlanId, reqVO.getPlanId())
+                .eqIfPresent(ReceivableDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(ReceivableDO::getContractId, reqVO.getContractId())
+                .eqIfPresent(ReceivableDO::getCheckStatus, reqVO.getCheckStatus())
+                .eqIfPresent(ReceivableDO::getProcessInstanceId, reqVO.getProcessInstanceId())
+                .betweenIfPresent(ReceivableDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(ReceivableDO::getReturnType, reqVO.getReturnType())
+                .eqIfPresent(ReceivableDO::getPrice, reqVO.getPrice())
+                .eqIfPresent(ReceivableDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .eqIfPresent(ReceivableDO::getBatchId, reqVO.getBatchId())
+                .eqIfPresent(ReceivableDO::getSort, reqVO.getSort())
+                .eqIfPresent(ReceivableDO::getDataScope, reqVO.getDataScope())
+                .eqIfPresent(ReceivableDO::getDataScopeDeptIds, reqVO.getDataScopeDeptIds())
+                .eqIfPresent(ReceivableDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(ReceivableDO::getRemark, reqVO.getRemark())
+                .betweenIfPresent(ReceivableDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ReceivableDO::getId));
+    }
+
+    default List<ReceivableDO> selectList(ReceivableExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<ReceivableDO>()
+                .eqIfPresent(ReceivableDO::getNo, reqVO.getNo())
+                .eqIfPresent(ReceivableDO::getPlanId, reqVO.getPlanId())
+                .eqIfPresent(ReceivableDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(ReceivableDO::getContractId, reqVO.getContractId())
+                .eqIfPresent(ReceivableDO::getCheckStatus, reqVO.getCheckStatus())
+                .eqIfPresent(ReceivableDO::getProcessInstanceId, reqVO.getProcessInstanceId())
+                .betweenIfPresent(ReceivableDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(ReceivableDO::getReturnType, reqVO.getReturnType())
+                .eqIfPresent(ReceivableDO::getPrice, reqVO.getPrice())
+                .eqIfPresent(ReceivableDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .eqIfPresent(ReceivableDO::getBatchId, reqVO.getBatchId())
+                .eqIfPresent(ReceivableDO::getSort, reqVO.getSort())
+                .eqIfPresent(ReceivableDO::getDataScope, reqVO.getDataScope())
+                .eqIfPresent(ReceivableDO::getDataScopeDeptIds, reqVO.getDataScopeDeptIds())
+                .eqIfPresent(ReceivableDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(ReceivableDO::getRemark, reqVO.getRemark())
+                .betweenIfPresent(ReceivableDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ReceivableDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableService.java
new file mode 100644
index 000000000..a673ec99e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableService.java
@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.crm.service.receivable;
+
+import java.util.*;
+import javax.validation.*;
+
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 回款管理 Service 接口
+ *
+ * @author 赤焰
+ */
+public interface ReceivableService {
+
+    /**
+     * 创建回款管理
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createReceivable(@Valid ReceivableCreateReqVO createReqVO);
+
+    /**
+     * 更新回款管理
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateReceivable(@Valid ReceivableUpdateReqVO updateReqVO);
+
+    /**
+     * 删除回款管理
+     *
+     * @param id 编号
+     */
+    void deleteReceivable(Long id);
+
+    /**
+     * 获得回款管理
+     *
+     * @param id 编号
+     * @return 回款管理
+     */
+    ReceivableDO getReceivable(Long id);
+
+    /**
+     * 获得回款管理列表
+     *
+     * @param ids 编号
+     * @return 回款管理列表
+     */
+    List<ReceivableDO> getReceivableList(Collection<Long> ids);
+
+    /**
+     * 获得回款管理分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 回款管理分页
+     */
+    PageResult<ReceivableDO> getReceivablePage(ReceivablePageReqVO pageReqVO);
+
+    /**
+     * 获得回款管理列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 回款管理列表
+     */
+    List<ReceivableDO> getReceivableList(ReceivableExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
new file mode 100644
index 000000000..2f5abe1fc
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
@@ -0,0 +1,87 @@
+package cn.iocoder.yudao.module.crm.service.receivable;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import java.util.*;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+
+import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+
+/**
+ * 回款管理 Service 实现类
+ *
+ * @author 赤焰
+ */
+@Service
+@Validated
+public class ReceivableServiceImpl implements ReceivableService {
+
+    @Resource
+    private ReceivableMapper receivableMapper;
+
+    @Override
+    public Long createReceivable(ReceivableCreateReqVO createReqVO) {
+        // 插入
+        ReceivableDO receivable = ReceivableConvert.INSTANCE.convert(createReqVO);
+        receivableMapper.insert(receivable);
+        // 返回
+        return receivable.getId();
+    }
+
+    @Override
+    public void updateReceivable(ReceivableUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateReceivableExists(updateReqVO.getId());
+        // 更新
+        ReceivableDO updateObj = ReceivableConvert.INSTANCE.convert(updateReqVO);
+        receivableMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteReceivable(Long id) {
+        // 校验存在
+        validateReceivableExists(id);
+        // 删除
+        receivableMapper.deleteById(id);
+    }
+
+    private void validateReceivableExists(Long id) {
+        if (receivableMapper.selectById(id) == null) {
+            throw exception(RECEIVABLE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ReceivableDO getReceivable(Long id) {
+        return receivableMapper.selectById(id);
+    }
+
+    @Override
+    public List<ReceivableDO> getReceivableList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return receivableMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<ReceivableDO> getReceivablePage(ReceivablePageReqVO pageReqVO) {
+        return receivableMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<ReceivableDO> getReceivableList(ReceivableExportReqVO exportReqVO) {
+        return receivableMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivableMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivableMapper.xml
new file mode 100644
index 000000000..b87beb08f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivableMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
new file mode 100644
index 000000000..3d1b47403
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
@@ -0,0 +1,279 @@
+package cn.iocoder.yudao.module.crm.service.receivable;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link ReceivableServiceImpl} 的单元测试类
+ *
+ * @author 赤焰
+ */
+@Import(ReceivableServiceImpl.class)
+public class ReceivableServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private ReceivableServiceImpl receivableService;
+
+    @Resource
+    private ReceivableMapper receivableMapper;
+
+    @Test
+    public void testCreateReceivable_success() {
+        // 准备参数
+        ReceivableCreateReqVO reqVO = randomPojo(ReceivableCreateReqVO.class);
+
+        // 调用
+        Long receivableId = receivableService.createReceivable(reqVO);
+        // 断言
+        assertNotNull(receivableId);
+        // 校验记录的属性是否正确
+        ReceivableDO receivable = receivableMapper.selectById(receivableId);
+        assertPojoEquals(reqVO, receivable);
+    }
+
+    @Test
+    public void testUpdateReceivable_success() {
+        // mock 数据
+        ReceivableDO dbReceivable = randomPojo(ReceivableDO.class);
+        receivableMapper.insert(dbReceivable);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        ReceivableUpdateReqVO reqVO = randomPojo(ReceivableUpdateReqVO.class, o -> {
+            o.setId(dbReceivable.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        receivableService.updateReceivable(reqVO);
+        // 校验是否更新正确
+        ReceivableDO receivable = receivableMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, receivable);
+    }
+
+    @Test
+    public void testUpdateReceivable_notExists() {
+        // 准备参数
+        ReceivableUpdateReqVO reqVO = randomPojo(ReceivableUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> receivableService.updateReceivable(reqVO), RECEIVABLE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteReceivable_success() {
+        // mock 数据
+        ReceivableDO dbReceivable = randomPojo(ReceivableDO.class);
+        receivableMapper.insert(dbReceivable);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbReceivable.getId();
+
+        // 调用
+        receivableService.deleteReceivable(id);
+       // 校验数据不存在了
+       assertNull(receivableMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteReceivable_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> receivableService.deleteReceivable(id), RECEIVABLE_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetReceivablePage() {
+       // mock 数据
+       ReceivableDO dbReceivable = randomPojo(ReceivableDO.class, o -> { // 等会查询到
+           o.setNo(null);
+           o.setPlanId(null);
+           o.setCustomerId(null);
+           o.setContractId(null);
+           o.setCheckStatus(null);
+           o.setProcessInstanceId(null);
+           o.setReturnTime(null);
+           o.setReturnType(null);
+           o.setPrice(null);
+           o.setOwnerUserId(null);
+           o.setBatchId(null);
+           o.setSort(null);
+           o.setDataScope(null);
+           o.setDataScopeDeptIds(null);
+           o.setStatus(null);
+           o.setRemark(null);
+           o.setCreateTime(null);
+       });
+       receivableMapper.insert(dbReceivable);
+       // 测试 no 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setNo(null)));
+       // 测试 planId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPlanId(null)));
+       // 测试 customerId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCustomerId(null)));
+       // 测试 contractId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setContractId(null)));
+       // 测试 checkStatus 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCheckStatus(null)));
+       // 测试 processInstanceId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setProcessInstanceId(null)));
+       // 测试 returnTime 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnTime(null)));
+       // 测试 returnType 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnType(null)));
+       // 测试 price 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPrice(null)));
+       // 测试 ownerUserId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setOwnerUserId(null)));
+       // 测试 batchId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setBatchId(null)));
+       // 测试 sort 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setSort(null)));
+       // 测试 dataScope 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScope(null)));
+       // 测试 dataScopeDeptIds 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScopeDeptIds(null)));
+       // 测试 status 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setStatus(null)));
+       // 测试 remark 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setRemark(null)));
+       // 测试 createTime 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCreateTime(null)));
+       // 准备参数
+       ReceivablePageReqVO reqVO = new ReceivablePageReqVO();
+       reqVO.setNo(null);
+       reqVO.setPlanId(null);
+       reqVO.setCustomerId(null);
+       reqVO.setContractId(null);
+       reqVO.setCheckStatus(null);
+       reqVO.setProcessInstanceId(null);
+       reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setReturnType(null);
+       reqVO.setPrice(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setBatchId(null);
+       reqVO.setSort(null);
+       reqVO.setDataScope(null);
+       reqVO.setDataScopeDeptIds(null);
+       reqVO.setStatus(null);
+       reqVO.setRemark(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<ReceivableDO> pageResult = receivableService.getReceivablePage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbReceivable, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetReceivableList() {
+       // mock 数据
+       ReceivableDO dbReceivable = randomPojo(ReceivableDO.class, o -> { // 等会查询到
+           o.setNo(null);
+           o.setPlanId(null);
+           o.setCustomerId(null);
+           o.setContractId(null);
+           o.setCheckStatus(null);
+           o.setProcessInstanceId(null);
+           o.setReturnTime(null);
+           o.setReturnType(null);
+           o.setPrice(null);
+           o.setOwnerUserId(null);
+           o.setBatchId(null);
+           o.setSort(null);
+           o.setDataScope(null);
+           o.setDataScopeDeptIds(null);
+           o.setStatus(null);
+           o.setRemark(null);
+           o.setCreateTime(null);
+       });
+       receivableMapper.insert(dbReceivable);
+       // 测试 no 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setNo(null)));
+       // 测试 planId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPlanId(null)));
+       // 测试 customerId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCustomerId(null)));
+       // 测试 contractId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setContractId(null)));
+       // 测试 checkStatus 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCheckStatus(null)));
+       // 测试 processInstanceId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setProcessInstanceId(null)));
+       // 测试 returnTime 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnTime(null)));
+       // 测试 returnType 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnType(null)));
+       // 测试 price 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPrice(null)));
+       // 测试 ownerUserId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setOwnerUserId(null)));
+       // 测试 batchId 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setBatchId(null)));
+       // 测试 sort 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setSort(null)));
+       // 测试 dataScope 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScope(null)));
+       // 测试 dataScopeDeptIds 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScopeDeptIds(null)));
+       // 测试 status 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setStatus(null)));
+       // 测试 remark 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setRemark(null)));
+       // 测试 createTime 不匹配
+       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCreateTime(null)));
+       // 准备参数
+       ReceivableExportReqVO reqVO = new ReceivableExportReqVO();
+       reqVO.setNo(null);
+       reqVO.setPlanId(null);
+       reqVO.setCustomerId(null);
+       reqVO.setContractId(null);
+       reqVO.setCheckStatus(null);
+       reqVO.setProcessInstanceId(null);
+       reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setReturnType(null);
+       reqVO.setPrice(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setBatchId(null);
+       reqVO.setSort(null);
+       reqVO.setDataScope(null);
+       reqVO.setDataScopeDeptIds(null);
+       reqVO.setStatus(null);
+       reqVO.setRemark(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       List<ReceivableDO> list = receivableService.getReceivableList(reqVO);
+       // 断言
+       assertEquals(1, list.size());
+       assertPojoEquals(dbReceivable, list.get(0));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
index 983e53ab3..97305a2e2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
@@ -1,3 +1,5 @@
 DELETE FROM "crm_contract";
 
-DELETE FROM "crm_clue";
\ No newline at end of file
+DELETE FROM "crm_clue";
+
+DELETE FROM "crm_receivable";
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
index 816264e66..cdea50a96 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -46,4 +46,30 @@ CREATE TABLE IF NOT EXISTS "crm_clue" (
   "deleted" bit NOT NULL DEFAULT FALSE,
   "tenant_id" bigint NOT NULL,
   PRIMARY KEY ("id")
-) COMMENT '线索表';
\ No newline at end of file
+) COMMENT '线索表';
+
+CREATE TABLE IF NOT EXISTS "crm_receivable" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "no" varchar,
+    "plan_id" bigint,
+    "customer_id" bigint,
+    "contract_id" bigint,
+    "check_status" int,
+    "process_instance_id" bigint,
+    "return_time" varchar,
+    "return_type" varchar,
+    "price" varchar,
+    "owner_user_id" bigint,
+    "batch_id" bigint,
+    "sort" int,
+    "data_scope" int,
+    "data_scope_dept_ids" varchar,
+    "status" int NOT NULL,
+    "remark" varchar,
+    "creator" varchar DEFAULT '',
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater" varchar DEFAULT '',
+    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted" bit NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
+) COMMENT '回款管理';

From e4943f0dfa547c72eede7a72dc9e93f04a763552 Mon Sep 17 00:00:00 2001
From: zyna <chenjidemenglin20@126.com>
Date: Fri, 20 Oct 2023 23:42:22 +0800
Subject: [PATCH 014/101] =?UTF-8?q?crm=E8=81=94=E7=B3=BB=E4=BA=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  26 ++++-
 .../module/crm/enums/ErrorCodeConstants.java  |   1 +
 .../admin/contact/ContactController.java      | 102 ++++++++++++++++++
 .../admin/contact/vo/ContactBaseVO.java       |  55 ++++++++++
 .../admin/contact/vo/ContactCreateReqVO.java  |  14 +++
 .../admin/contact/vo/ContactExcelVO.java      |  59 ++++++++++
 .../admin/contact/vo/ContactExportReqVO.java  |  55 ++++++++++
 .../admin/contact/vo/ContactPageReqVO.java    |  57 ++++++++++
 .../admin/contact/vo/ContactRespVO.java       |  20 ++++
 .../admin/contact/vo/ContactUpdateReqVO.java  |  17 +++
 .../crm/convert/contact/ContactConvert.java   |  34 ++++++
 .../crm/dal/dataobject/contact/ContactDO.java |  76 +++++++++++++
 .../crm/dal/mysql/contact/ContactMapper.java  |  54 ++++++++++
 .../crm/service/contact/ContactService.java   |  70 ++++++++++++
 .../service/contact/ContactServiceImpl.java   |  88 +++++++++++++++
 .../mapper/contact/ContactMapper.xml          |  12 +++
 16 files changed, 739 insertions(+), 1 deletion(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/contact/ContactMapper.xml

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 6c5594068..c01abbe54 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -60,4 +60,28 @@ CREATE TABLE `crm_clue`  (
      `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
      `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
      PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
\ No newline at end of file
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
+
+-- `ruoyi-vue-pro`.crm_contact definition
+
+CREATE TABLE `crm_contact` (
+                               `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                               `name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系人名称',
+                               `next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
+                               `mobile` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
+                               `telephone` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
+                               `email` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电子邮箱',
+                               `post` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '职务',
+                               `customer_id` bigint(20) DEFAULT NULL COMMENT '客户编号',
+                               `address` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
+                               `remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+                               `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人',
+                               `owner_user_id` bigint DEFAULT NULL COMMENT '负责人用户编号',
+                               `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
+                               `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
+                               `last_time` timestamp NULL DEFAULT NULL COMMENT '最后跟进时间',
+                               `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人',
+                               `deleted` bit(1) NOT NULL DEFAULT b'0',
+                               `tenant_id` bigint DEFAULT NULL,
+                               PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='crm联系人';
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 3538b16ee..18c35dda7 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -14,5 +14,6 @@ public interface ErrorCodeConstants {
 
     // TODO @wanwan:要单独一个分段噢
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_000_001, "线索不存在");
+    ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_000_002, "线索不存在");
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
new file mode 100644
index 000000000..bafec03c1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -0,0 +1,102 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.service.contact.ContactService;
+
+@Tag(name = "管理后台 - crm联系人")
+@RestController
+@RequestMapping("/crm/contact")
+@Validated
+public class ContactController {
+
+    @Resource
+    private ContactService contactService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建crm联系人")
+    @PreAuthorize("@ss.hasPermission('crm:contact:create')")
+    public CommonResult<Long> createContact(@Valid @RequestBody ContactCreateReqVO createReqVO) {
+        return success(contactService.createContact(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新crm联系人")
+    @PreAuthorize("@ss.hasPermission('crm:contact:update')")
+    public CommonResult<Boolean> updateContact(@Valid @RequestBody ContactUpdateReqVO updateReqVO) {
+        contactService.updateContact(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除crm联系人")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:contact:delete')")
+    public CommonResult<Boolean> deleteContact(@RequestParam("id") Long id) {
+        contactService.deleteContact(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得crm联系人")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
+    public CommonResult<ContactRespVO> getContact(@RequestParam("id") Long id) {
+        ContactDO contact = contactService.getContact(id);
+        return success(ContactConvert.INSTANCE.convert(contact));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得crm联系人列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
+    public CommonResult<List<ContactRespVO>> getContactList(@RequestParam("ids") Collection<Long> ids) {
+        List<ContactDO> list = contactService.getContactList(ids);
+        return success(ContactConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得crm联系人分页")
+    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
+    public CommonResult<PageResult<ContactRespVO>> getContactPage(@Valid ContactPageReqVO pageVO) {
+        PageResult<ContactDO> pageResult = contactService.getContactPage(pageVO);
+        return success(ContactConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出crm联系人 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:contact:export')")
+    @OperateLog(type = EXPORT)
+    public void exportContactExcel(@Valid ContactExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<ContactDO> list = contactService.getContactList(exportReqVO);
+        // 导出 Excel
+        List<ContactExcelVO> datas = ContactConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "crm联系人.xls", "数据", ContactExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
new file mode 100644
index 000000000..5b39c1f41
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import javax.validation.constraints.*;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * crm联系人 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ContactBaseVO {
+
+    @Schema(description = "联系人名称", example = "张三")
+    @NotNull(message = "姓名不能为空")
+    private String name;
+
+    @Schema(description = "下次联系时间")
+    private LocalDateTime nextTime;
+
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "电话")
+    private String telephone;
+
+    @Schema(description = "电子邮箱")
+    private String email;
+
+    @Schema(description = "职务")
+    private String post;
+
+    @Schema(description = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @Schema(description = "负责人用户编号", example = "7648")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime lastTime;
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
new file mode 100644
index 000000000..424d945dc
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - crm联系人创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactCreateReqVO extends ContactBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
new file mode 100644
index 000000000..3ac5f3765
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * crm联系人 Excel VO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ContactExcelVO {
+
+    @ExcelProperty("主键")
+    private Long id;
+
+    @ExcelProperty("联系人名称")
+    private String name;
+
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime nextTime;
+
+    @ExcelProperty("手机号")
+    private String mobile;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("电子邮箱")
+    private String email;
+
+    @ExcelProperty("职务")
+    private String post;
+
+    @ExcelProperty("客户编号")
+    private Long customerId;
+
+    @ExcelProperty("地址")
+    private String address;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("负责人用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime lastTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
new file mode 100644
index 000000000..df4bd59e1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import java.time.LocalDateTime;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - crm联系人 Excel 导出 Request VO,参数和 ContactPageReqVO 是一致的")
+@Data
+public class ContactExportReqVO {
+
+    @Schema(description = "联系人名称", example = "张三")
+    private String name;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] nextTime;
+
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "电话")
+    private String telephone;
+
+    @Schema(description = "电子邮箱")
+    private String email;
+
+    @Schema(description = "职务")
+    private String post;
+
+    @Schema(description = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @Schema(description = "负责人用户编号", example = "7648")
+    private Long ownerUserId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] lastTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
new file mode 100644
index 000000000..058483926
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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 = "管理后台 - crm联系人分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactPageReqVO extends PageParam {
+
+    @Schema(description = "联系人名称", example = "张三")
+    private String name;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] nextTime;
+
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "电话")
+    private String telephone;
+
+    @Schema(description = "电子邮箱")
+    private String email;
+
+    @Schema(description = "职务")
+    private String post;
+
+    @Schema(description = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @Schema(description = "负责人用户编号", example = "7648")
+    private Long ownerUserId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] lastTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
new file mode 100644
index 000000000..acb6a737d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - crm联系人 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactRespVO extends ContactBaseVO {
+
+    @Schema(description = "主键", example = "23210")
+    private Long id;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+    @Schema(description = "创建人")
+    private String creator;
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
new file mode 100644
index 000000000..19db67297
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - crm联系人更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactUpdateReqVO extends ContactBaseVO {
+
+    @Schema(description = "主键", example = "23210")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
new file mode 100644
index 000000000..3892806c0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.contact;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+
+/**
+ * crm联系人 Convert
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ContactConvert {
+
+    ContactConvert INSTANCE = Mappers.getMapper(ContactConvert.class);
+
+    ContactDO convert(ContactCreateReqVO bean);
+
+    ContactDO convert(ContactUpdateReqVO bean);
+
+    ContactRespVO convert(ContactDO bean);
+
+    List<ContactRespVO> convertList(List<ContactDO> list);
+
+    PageResult<ContactRespVO> convertPage(PageResult<ContactDO> page);
+
+    List<ContactExcelVO> convertList02(List<ContactDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
new file mode 100644
index 000000000..ee735ae61
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
@@ -0,0 +1,76 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * crm联系人 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("crm_contact")
+@KeySequence("crm_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ContactDO extends BaseDO {
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 联系人名称
+     */
+    private String name;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime nextTime;
+    /**
+     * 手机号
+     */
+    private String mobile;
+    /**
+     * 电话
+     */
+    private String telephone;
+    /**
+     * 电子邮箱
+     */
+    private String email;
+    /**
+     * 职务
+     */
+    private String post;
+    /**
+     * 客户编号
+     */
+    private Long customerId;
+    /**
+     * 地址
+     */
+    private String address;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 负责人用户编号
+     */
+    private Long ownerUserId;
+    /**
+     * 最后跟进时间
+     */
+    private LocalDateTime lastTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
new file mode 100644
index 000000000..dfefeba5d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.contact;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+
+/**
+ * crm联系人 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ContactMapper extends BaseMapperX<ContactDO> {
+
+    default PageResult<ContactDO> selectPage(ContactPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ContactDO>()
+                .likeIfPresent(ContactDO::getName, reqVO.getName())
+                .betweenIfPresent(ContactDO::getNextTime, reqVO.getNextTime())
+                .eqIfPresent(ContactDO::getMobile, reqVO.getMobile())
+                .eqIfPresent(ContactDO::getTelephone, reqVO.getTelephone())
+                .eqIfPresent(ContactDO::getEmail, reqVO.getEmail())
+                .eqIfPresent(ContactDO::getPost, reqVO.getPost())
+                .eqIfPresent(ContactDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(ContactDO::getAddress, reqVO.getAddress())
+                .eqIfPresent(ContactDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ContactDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(ContactDO::getCreateTime, reqVO.getCreateTime())
+                .betweenIfPresent(ContactDO::getLastTime, reqVO.getLastTime())
+                .orderByDesc(ContactDO::getId));
+    }
+
+    default List<ContactDO> selectList(ContactExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<ContactDO>()
+                .likeIfPresent(ContactDO::getName, reqVO.getName())
+                .betweenIfPresent(ContactDO::getNextTime, reqVO.getNextTime())
+                .eqIfPresent(ContactDO::getMobile, reqVO.getMobile())
+                .eqIfPresent(ContactDO::getTelephone, reqVO.getTelephone())
+                .eqIfPresent(ContactDO::getEmail, reqVO.getEmail())
+                .eqIfPresent(ContactDO::getPost, reqVO.getPost())
+                .eqIfPresent(ContactDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(ContactDO::getAddress, reqVO.getAddress())
+                .eqIfPresent(ContactDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ContactDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(ContactDO::getCreateTime, reqVO.getCreateTime())
+                .betweenIfPresent(ContactDO::getLastTime, reqVO.getLastTime())
+                .orderByDesc(ContactDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
new file mode 100644
index 000000000..97a221ee4
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.crm.service.contact;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * crm联系人 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ContactService {
+
+    /**
+     * 创建crm联系人
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createContact(@Valid ContactCreateReqVO createReqVO);
+
+    /**
+     * 更新crm联系人
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateContact(@Valid ContactUpdateReqVO updateReqVO);
+
+    /**
+     * 删除crm联系人
+     *
+     * @param id 编号
+     */
+    void deleteContact(Long id);
+
+    /**
+     * 获得crm联系人
+     *
+     * @param id 编号
+     * @return crm联系人
+     */
+    ContactDO getContact(Long id);
+
+    /**
+     * 获得crm联系人列表
+     *
+     * @param ids 编号
+     * @return crm联系人列表
+     */
+    List<ContactDO> getContactList(Collection<Long> ids);
+
+    /**
+     * 获得crm联系人分页
+     *
+     * @param pageReqVO 分页查询
+     * @return crm联系人分页
+     */
+    PageResult<ContactDO> getContactPage(ContactPageReqVO pageReqVO);
+
+    /**
+     * 获得crm联系人列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return crm联系人列表
+     */
+    List<ContactDO> getContactList(ContactExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
new file mode 100644
index 000000000..85ce3c269
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.crm.service.contact;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+
+/**
+ * crm联系人 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ContactServiceImpl implements ContactService {
+
+    @Resource
+    private ContactMapper contactMapper;
+
+    @Override
+    public Long createContact(ContactCreateReqVO createReqVO) {
+        // 插入
+        ContactDO contact = ContactConvert.INSTANCE.convert(createReqVO);
+        contactMapper.insert(contact);
+        // 返回
+        return contact.getId();
+    }
+
+    @Override
+    public void updateContact(ContactUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateContactExists(updateReqVO.getId());
+        // 更新
+        ContactDO updateObj = ContactConvert.INSTANCE.convert(updateReqVO);
+        contactMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteContact(Long id) {
+        // 校验存在
+        validateContactExists(id);
+        // 删除
+        contactMapper.deleteById(id);
+    }
+
+    private void validateContactExists(Long id) {
+        if (contactMapper.selectById(id) == null) {
+            throw exception(CONTACT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ContactDO getContact(Long id) {
+        return contactMapper.selectById(id);
+    }
+
+    @Override
+    public List<ContactDO> getContactList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return contactMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<ContactDO> getContactPage(ContactPageReqVO pageReqVO) {
+        return contactMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<ContactDO> getContactList(ContactExportReqVO exportReqVO) {
+        return contactMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/contact/ContactMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/contact/ContactMapper.xml
new file mode 100644
index 000000000..e2c69ca78
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/contact/ContactMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

From 34a5785768d6f14c0f05ab64c10ef095badfc3c3 Mon Sep 17 00:00:00 2001
From: liuhongfeng <291117974@qq.com>
Date: Fri, 20 Oct 2023 23:48:19 +0800
Subject: [PATCH 015/101] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91?=
 =?UTF-8?q?=E5=9B=9E=E6=AC=BE=E8=AE=A1=E5=88=92=20=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  35 +++
 sql/mysql/crm_menu.sql                        |  61 +++++
 .../module/crm/enums/ErrorCodeConstants.java  |   2 +
 .../receivable/ReceivablePlanController.java  | 105 ++++++++
 .../receivable/vo/ReceivablePlanBaseVO.java   |  68 ++++++
 .../vo/ReceivablePlanCreateReqVO.java         |  14 ++
 .../receivable/vo/ReceivablePlanExcelVO.java  |  75 ++++++
 .../vo/ReceivablePlanExportReqVO.java         |  52 ++++
 .../vo/ReceivablePlanPageReqVO.java           |  54 ++++
 .../receivable/vo/ReceivablePlanRespVO.java   |  19 ++
 .../vo/ReceivablePlanUpdateReqVO.java         |  18 ++
 .../receivable/ReceivablePlanConvert.java     |  34 +++
 .../receivable/ReceivablePlanDO.java          |  94 +++++++
 .../receivable/ReceivablePlanMapper.java      |  52 ++++
 .../receivable/ReceivablePlanService.java     |  70 ++++++
 .../receivable/ReceivablePlanServiceImpl.java |  93 +++++++
 .../receivable/ReceivablePlanMapper.xml       |  12 +
 .../ReceivablePlanServiceImplTest.java        | 231 ++++++++++++++++++
 .../src/test/resources/sql/clean.sql          |   2 +
 .../src/test/resources/sql/create_tables.sql  |  25 ++
 20 files changed, 1116 insertions(+)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivablePlanConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivablePlanMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 699f69307..8b3b72645 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -93,3 +93,38 @@ CREATE TABLE `crm_receivable`  (
    `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
    PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回款管理' ROW_FORMAT = DYNAMIC;
+
+
+-- ----------------------------
+-- 回款计划表
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_receivable_plan`;
+CREATE TABLE `crm_receivable_plan`  (
+    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `index_no` bigint(20) NULL DEFAULT NULL COMMENT '期数',
+    `receivable_id` bigint(20) NULL DEFAULT NULL COMMENT '回款ID',
+    `status` tinyint(4) NOT NULL COMMENT '完成状态',
+    `check_status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批状态',
+    `process_instance_id` bigint(20) NULL DEFAULT NULL COMMENT '工作流编号',
+    `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '计划回款金额',
+    `return_time` datetime NULL DEFAULT NULL COMMENT '计划回款日期',
+    `remind_days` bigint(20) NULL DEFAULT NULL COMMENT '提前几天提醒',
+    `remind_time` datetime NULL DEFAULT NULL COMMENT '提醒日期',
+    `customer_id` bigint(20) NULL DEFAULT NULL COMMENT '客户ID',
+    `contract_id` bigint(20) NULL DEFAULT NULL COMMENT '合同ID',
+    `owner_user_id` bigint(20) NULL DEFAULT NULL COMMENT '负责人',
+    `sort` int(11) NULL DEFAULT NULL COMMENT '显示顺序',
+    `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
+    `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+    `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回款计划' ROW_FORMAT = DYNAMIC;
+
+
+
+
+
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index 25c07c6a8..22680e81b 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -184,3 +184,64 @@ VALUES (
            '回款管理导出', 'crm:receivable:export', 3, 5, @parentId,
            '', '', '', 0
        );
+
+
+-- ----------------------------
+-- 回款计划菜单
+-- ----------------------------
+
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+           '回款计划管理', '', 2, 0, 2375,
+           'receivable-plan', '', 'crm/receivablePlan/index', 0, 'ReceivablePlan'
+       );
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款计划查询', 'crm:receivable-plan:query', 3, 1, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款计划创建', 'crm:receivable-plan:create', 3, 2, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款计划更新', 'crm:receivable-plan:update', 3, 3, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款计划删除', 'crm:receivable-plan:delete', 3, 4, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '回款计划导出', 'crm:receivable-plan:export', 3, 5, @parentId,
+           '', '', '', 0
+       );
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 6c0d03d93..c0d996f17 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -17,4 +17,6 @@ public interface ErrorCodeConstants {
 
     ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_030_000_001, "回款管理不存在");
 
+    ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_040_000_001, "回款计划不存在");
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
new file mode 100644
index 000000000..ae21a952d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
@@ -0,0 +1,105 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.convert.receivable.ReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.service.receivable.ReceivablePlanService;
+
+/**
+ * @author 赤焰
+ */
+@Tag(name = "管理后台 - 回款计划")
+@RestController
+@RequestMapping("/crm/receivable-plan")
+@Validated
+public class ReceivablePlanController {
+
+    @Resource
+    private ReceivablePlanService receivablePlanService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建回款计划")
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:create')")
+    public CommonResult<Long> createReceivablePlan(@Valid @RequestBody ReceivablePlanCreateReqVO createReqVO) {
+        return success(receivablePlanService.createReceivablePlan(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新回款计划")
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:update')")
+    public CommonResult<Boolean> updateReceivablePlan(@Valid @RequestBody ReceivablePlanUpdateReqVO updateReqVO) {
+        receivablePlanService.updateReceivablePlan(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除回款计划")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:delete')")
+    public CommonResult<Boolean> deleteReceivablePlan(@RequestParam("id") Long id) {
+        receivablePlanService.deleteReceivablePlan(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得回款计划")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
+    public CommonResult<ReceivablePlanRespVO> getReceivablePlan(@RequestParam("id") Long id) {
+        ReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(id);
+        return success(ReceivablePlanConvert.INSTANCE.convert(receivablePlan));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得回款计划列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
+    public CommonResult<List<ReceivablePlanRespVO>> getReceivablePlanList(@RequestParam("ids") Collection<Long> ids) {
+        List<ReceivablePlanDO> list = receivablePlanService.getReceivablePlanList(ids);
+        return success(ReceivablePlanConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得回款计划分页")
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
+    public CommonResult<PageResult<ReceivablePlanRespVO>> getReceivablePlanPage(@Valid ReceivablePlanPageReqVO pageVO) {
+        PageResult<ReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(pageVO);
+        return success(ReceivablePlanConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出回款计划 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:export')")
+    @OperateLog(type = EXPORT)
+    public void exportReceivablePlanExcel(@Valid ReceivablePlanExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<ReceivablePlanDO> list = receivablePlanService.getReceivablePlanList(exportReqVO);
+        // 导出 Excel
+        List<ReceivablePlanExcelVO> datas = ReceivablePlanConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "回款计划.xls", "数据", ReceivablePlanExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
new file mode 100644
index 000000000..638934a8e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import javax.validation.constraints.*;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 回款计划 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ReceivablePlanBaseVO {
+
+    @Schema(description = "期数")
+    private Long indexNo;
+
+    @Schema(description = "回款ID", example = "19852")
+    private Long receivableId;
+
+    @Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    //@NotNull(message = "完成状态不能为空")
+    private Integer status;
+
+    @Schema(description = "审批状态", example = "1")
+    private String checkStatus;
+
+    @Schema(description = "工作流编号", example = "8909")
+    private Long processInstanceId;
+
+    @Schema(description = "计划回款金额", example = "29675")
+    private BigDecimal price;
+
+    @Schema(description = "计划回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime returnTime;
+
+    @Schema(description = "提前几天提醒")
+    private Long remindDays;
+
+    @Schema(description = "提醒日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime remindTime;
+
+    @Schema(description = "客户ID", example = "18026")
+    private Long customerId;
+
+    @Schema(description = "合同ID", example = "3473")
+    private Long contractId;
+
+    @Schema(description = "负责人", example = "17828")
+    private Long ownerUserId;
+
+    @Schema(description = "显示顺序")
+    private Integer sort;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java
new file mode 100644
index 000000000..0d4fce3c8
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 回款计划创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ReceivablePlanCreateReqVO extends ReceivablePlanBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
new file mode 100644
index 000000000..e12d5a1df
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+
+/**
+ * 回款计划 Excel VO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ReceivablePlanExcelVO {
+
+    @ExcelProperty("ID")
+    private Long id;
+
+    @ExcelProperty("期数")
+    private Long indexNo;
+
+    @ExcelProperty("回款ID")
+    private Long receivableId;
+
+    @ExcelProperty(value = "完成状态", converter = DictConvert.class)
+    @DictFormat("common_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Integer status;
+
+    @ExcelProperty(value = "审批状态", converter = DictConvert.class)
+    @DictFormat("crm_receivable_check_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private String checkStatus;
+
+    //@ExcelProperty("工作流编号")
+    //private Long processInstanceId;
+
+    @ExcelProperty("计划回款金额")
+    private BigDecimal price;
+
+    @ExcelProperty("计划回款日期")
+    private LocalDateTime returnTime;
+
+    @ExcelProperty("提前几天提醒")
+    private Long remindDays;
+
+    @ExcelProperty("提醒日期")
+    private LocalDateTime remindTime;
+
+    @ExcelProperty("客户ID")
+    private Long customerId;
+
+    @ExcelProperty("合同ID")
+    private Long contractId;
+
+    @ExcelProperty("负责人")
+    private Long ownerUserId;
+
+    //@ExcelProperty("显示顺序")
+    //private Integer sort;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
new file mode 100644
index 000000000..ca5d3f553
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import java.time.LocalDateTime;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 回款计划 Excel 导出 Request VO,参数和 ReceivablePlanPageReqVO 是一致的")
+@Data
+public class ReceivablePlanExportReqVO {
+
+    @Schema(description = "期数")
+    private Long indexNo;
+
+    @Schema(description = "完成状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "审批状态", example = "1")
+    private String checkStatus;
+
+    @Schema(description = "计划回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] returnTime;
+
+    @Schema(description = "提前几天提醒")
+    private Long remindDays;
+
+    @Schema(description = "提醒日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] remindTime;
+
+    @Schema(description = "客户ID", example = "18026")
+    private Long customerId;
+
+    @Schema(description = "合同ID", example = "3473")
+    private Long contractId;
+
+    @Schema(description = "负责人", example = "17828")
+    private Long ownerUserId;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
new file mode 100644
index 000000000..2c1166d1c
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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 ReceivablePlanPageReqVO extends PageParam {
+
+    @Schema(description = "期数")
+    private Long indexNo;
+
+    @Schema(description = "完成状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "审批状态", example = "1")
+    private String checkStatus;
+
+    @Schema(description = "计划回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] returnTime;
+
+    @Schema(description = "提前几天提醒")
+    private Long remindDays;
+
+    @Schema(description = "提醒日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] remindTime;
+
+    @Schema(description = "客户ID", example = "18026")
+    private Long customerId;
+
+    @Schema(description = "合同ID", example = "3473")
+    private Long contractId;
+
+    @Schema(description = "负责人", example = "17828")
+    private Long ownerUserId;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java
new file mode 100644
index 000000000..d96f631de
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 回款计划 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ReceivablePlanRespVO extends ReceivablePlanBaseVO {
+
+    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25153")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java
new file mode 100644
index 000000000..75f4ecca0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 回款计划更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ReceivablePlanUpdateReqVO extends ReceivablePlanBaseVO {
+
+    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25153")
+    @NotNull(message = "ID不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivablePlanConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivablePlanConvert.java
new file mode 100644
index 000000000..81ec646f3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivablePlanConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.receivable;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
+
+/**
+ * 回款计划 Convert
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ReceivablePlanConvert {
+
+    ReceivablePlanConvert INSTANCE = Mappers.getMapper(ReceivablePlanConvert.class);
+
+    ReceivablePlanDO convert(ReceivablePlanCreateReqVO bean);
+
+    ReceivablePlanDO convert(ReceivablePlanUpdateReqVO bean);
+
+    ReceivablePlanRespVO convert(ReceivablePlanDO bean);
+
+    List<ReceivablePlanRespVO> convertList(List<ReceivablePlanDO> list);
+
+    PageResult<ReceivablePlanRespVO> convertPage(PageResult<ReceivablePlanDO> page);
+
+    List<ReceivablePlanExcelVO> convertList02(List<ReceivablePlanDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
new file mode 100644
index 000000000..4447df053
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;
+
+import lombok.*;
+import java.util.*;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 回款计划 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("crm_receivable_plan")
+@KeySequence("crm_receivable_plan_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReceivablePlanDO extends BaseDO {
+
+    /**
+     * ID
+     */
+    @TableId
+    private Long id;
+    /**
+     * 期数
+     */
+    private Long indexNo;
+    /**
+     * 回款ID
+     */
+    private Long receivableId;
+    /**
+     * 完成状态
+     *
+     * 枚举 {@link TODO common_status 对应的类}
+     */
+    private Integer status;
+    /**
+     * 审批状态
+     *
+     * 枚举 {@link TODO crm_receivable_check_status 对应的类}
+     */
+    private String checkStatus;
+    /**
+     * 工作流编号
+     */
+    private Long processInstanceId;
+    /**
+     * 计划回款金额
+     */
+    private BigDecimal price;
+    /**
+     * 计划回款日期
+     */
+    private LocalDateTime returnTime;
+    /**
+     * 提前几天提醒
+     */
+    private Long remindDays;
+    /**
+     * 提醒日期
+     */
+    private LocalDateTime remindTime;
+    /**
+     * 客户ID
+     */
+    private Long customerId;
+    /**
+     * 合同ID
+     */
+    private Long contractId;
+    /**
+     * 负责人
+     */
+    private Long ownerUserId;
+    /**
+     * 显示顺序
+     */
+    private Integer sort;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
new file mode 100644
index 000000000..ac35133a7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+
+/**
+ * 回款计划 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ReceivablePlanMapper extends BaseMapperX<ReceivablePlanDO> {
+
+    default PageResult<ReceivablePlanDO> selectPage(ReceivablePlanPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ReceivablePlanDO>()
+                .eqIfPresent(ReceivablePlanDO::getIndexNo, reqVO.getIndexNo())
+                .eqIfPresent(ReceivablePlanDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(ReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
+                .betweenIfPresent(ReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(ReceivablePlanDO::getRemindDays, reqVO.getRemindDays())
+                .betweenIfPresent(ReceivablePlanDO::getRemindTime, reqVO.getRemindTime())
+                .eqIfPresent(ReceivablePlanDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(ReceivablePlanDO::getContractId, reqVO.getContractId())
+                .eqIfPresent(ReceivablePlanDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .eqIfPresent(ReceivablePlanDO::getRemark, reqVO.getRemark())
+                .betweenIfPresent(ReceivablePlanDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ReceivablePlanDO::getId));
+    }
+
+    default List<ReceivablePlanDO> selectList(ReceivablePlanExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<ReceivablePlanDO>()
+                .eqIfPresent(ReceivablePlanDO::getIndexNo, reqVO.getIndexNo())
+                .eqIfPresent(ReceivablePlanDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(ReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
+                .betweenIfPresent(ReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(ReceivablePlanDO::getRemindDays, reqVO.getRemindDays())
+                .betweenIfPresent(ReceivablePlanDO::getRemindTime, reqVO.getRemindTime())
+                .eqIfPresent(ReceivablePlanDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(ReceivablePlanDO::getContractId, reqVO.getContractId())
+                .eqIfPresent(ReceivablePlanDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .eqIfPresent(ReceivablePlanDO::getRemark, reqVO.getRemark())
+                .betweenIfPresent(ReceivablePlanDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ReceivablePlanDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanService.java
new file mode 100644
index 000000000..163ebc26a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanService.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.crm.service.receivable;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 回款计划 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ReceivablePlanService {
+
+    /**
+     * 创建回款计划
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createReceivablePlan(@Valid ReceivablePlanCreateReqVO createReqVO);
+
+    /**
+     * 更新回款计划
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateReceivablePlan(@Valid ReceivablePlanUpdateReqVO updateReqVO);
+
+    /**
+     * 删除回款计划
+     *
+     * @param id 编号
+     */
+    void deleteReceivablePlan(Long id);
+
+    /**
+     * 获得回款计划
+     *
+     * @param id 编号
+     * @return 回款计划
+     */
+    ReceivablePlanDO getReceivablePlan(Long id);
+
+    /**
+     * 获得回款计划列表
+     *
+     * @param ids 编号
+     * @return 回款计划列表
+     */
+    List<ReceivablePlanDO> getReceivablePlanList(Collection<Long> ids);
+
+    /**
+     * 获得回款计划分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 回款计划分页
+     */
+    PageResult<ReceivablePlanDO> getReceivablePlanPage(ReceivablePlanPageReqVO pageReqVO);
+
+    /**
+     * 获得回款计划列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 回款计划列表
+     */
+    List<ReceivablePlanDO> getReceivablePlanList(ReceivablePlanExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
new file mode 100644
index 000000000..1a1462388
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.crm.service.receivable;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.convert.receivable.ReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+
+/**
+ * 回款计划 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ReceivablePlanServiceImpl implements ReceivablePlanService {
+
+    @Resource
+    private ReceivablePlanMapper receivablePlanMapper;
+
+    @Override
+    public Long createReceivablePlan(ReceivablePlanCreateReqVO createReqVO) {
+        // 插入
+        ReceivablePlanDO receivablePlan = ReceivablePlanConvert.INSTANCE.convert(createReqVO);
+        if(ObjectUtil.isNull(receivablePlan.getStatus())){
+            receivablePlan.setStatus(CommonStatusEnum.ENABLE.getStatus());
+        }
+        receivablePlanMapper.insert(receivablePlan);
+        // 返回
+        return receivablePlan.getId();
+    }
+
+    @Override
+    public void updateReceivablePlan(ReceivablePlanUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateReceivablePlanExists(updateReqVO.getId());
+        // 更新
+        ReceivablePlanDO updateObj = ReceivablePlanConvert.INSTANCE.convert(updateReqVO);
+        receivablePlanMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteReceivablePlan(Long id) {
+        // 校验存在
+        validateReceivablePlanExists(id);
+        // 删除
+        receivablePlanMapper.deleteById(id);
+    }
+
+    private void validateReceivablePlanExists(Long id) {
+        if (receivablePlanMapper.selectById(id) == null) {
+            throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ReceivablePlanDO getReceivablePlan(Long id) {
+        return receivablePlanMapper.selectById(id);
+    }
+
+    @Override
+    public List<ReceivablePlanDO> getReceivablePlanList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return receivablePlanMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<ReceivablePlanDO> getReceivablePlanPage(ReceivablePlanPageReqVO pageReqVO) {
+        return receivablePlanMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<ReceivablePlanDO> getReceivablePlanList(ReceivablePlanExportReqVO exportReqVO) {
+        return receivablePlanMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivablePlanMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivablePlanMapper.xml
new file mode 100644
index 000000000..f0e4c2e84
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivablePlanMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
new file mode 100644
index 000000000..0d90fd1aa
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
@@ -0,0 +1,231 @@
+package cn.iocoder.yudao.module.crm.service.receivable;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link ReceivablePlanServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(ReceivablePlanServiceImpl.class)
+public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private ReceivablePlanServiceImpl receivablePlanService;
+
+    @Resource
+    private ReceivablePlanMapper receivablePlanMapper;
+
+    @Test
+    public void testCreateReceivablePlan_success() {
+        // 准备参数
+        ReceivablePlanCreateReqVO reqVO = randomPojo(ReceivablePlanCreateReqVO.class);
+
+        // 调用
+        Long receivablePlanId = receivablePlanService.createReceivablePlan(reqVO);
+        // 断言
+        assertNotNull(receivablePlanId);
+        // 校验记录的属性是否正确
+        ReceivablePlanDO receivablePlan = receivablePlanMapper.selectById(receivablePlanId);
+        assertPojoEquals(reqVO, receivablePlan);
+    }
+
+    @Test
+    public void testUpdateReceivablePlan_success() {
+        // mock 数据
+        ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class);
+        receivablePlanMapper.insert(dbReceivablePlan);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        ReceivablePlanUpdateReqVO reqVO = randomPojo(ReceivablePlanUpdateReqVO.class, o -> {
+            o.setId(dbReceivablePlan.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        receivablePlanService.updateReceivablePlan(reqVO);
+        // 校验是否更新正确
+        ReceivablePlanDO receivablePlan = receivablePlanMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, receivablePlan);
+    }
+
+    @Test
+    public void testUpdateReceivablePlan_notExists() {
+        // 准备参数
+        ReceivablePlanUpdateReqVO reqVO = randomPojo(ReceivablePlanUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> receivablePlanService.updateReceivablePlan(reqVO), RECEIVABLE_PLAN_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteReceivablePlan_success() {
+        // mock 数据
+        ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class);
+        receivablePlanMapper.insert(dbReceivablePlan);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbReceivablePlan.getId();
+
+        // 调用
+        receivablePlanService.deleteReceivablePlan(id);
+       // 校验数据不存在了
+       assertNull(receivablePlanMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteReceivablePlan_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> receivablePlanService.deleteReceivablePlan(id), RECEIVABLE_PLAN_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetReceivablePlanPage() {
+       // mock 数据
+       ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class, o -> { // 等会查询到
+           o.setIndexNo(null);
+           o.setStatus(null);
+           o.setCheckStatus(null);
+           o.setReturnTime(null);
+           o.setRemindDays(null);
+           o.setRemindTime(null);
+           o.setCustomerId(null);
+           o.setContractId(null);
+           o.setOwnerUserId(null);
+           o.setRemark(null);
+           o.setCreateTime(null);
+       });
+       receivablePlanMapper.insert(dbReceivablePlan);
+       // 测试 indexNo 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setIndexNo(null)));
+       // 测试 status 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
+       // 测试 checkStatus 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCheckStatus(null)));
+       // 测试 returnTime 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setReturnTime(null)));
+       // 测试 remindDays 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindDays(null)));
+       // 测试 remindTime 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindTime(null)));
+       // 测试 customerId 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCustomerId(null)));
+       // 测试 contractId 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setContractId(null)));
+       // 测试 ownerUserId 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setOwnerUserId(null)));
+       // 测试 remark 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemark(null)));
+       // 测试 createTime 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
+       // 准备参数
+       ReceivablePlanPageReqVO reqVO = new ReceivablePlanPageReqVO();
+       reqVO.setIndexNo(null);
+       reqVO.setStatus(null);
+       reqVO.setCheckStatus(null);
+       reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setRemindDays(null);
+       reqVO.setRemindTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCustomerId(null);
+       reqVO.setContractId(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setRemark(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<ReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbReceivablePlan, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetReceivablePlanList() {
+       // mock 数据
+       ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class, o -> { // 等会查询到
+           o.setIndexNo(null);
+           o.setStatus(null);
+           o.setCheckStatus(null);
+           o.setReturnTime(null);
+           o.setRemindDays(null);
+           o.setRemindTime(null);
+           o.setCustomerId(null);
+           o.setContractId(null);
+           o.setOwnerUserId(null);
+           o.setRemark(null);
+           o.setCreateTime(null);
+       });
+       receivablePlanMapper.insert(dbReceivablePlan);
+       // 测试 indexNo 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setIndexNo(null)));
+       // 测试 status 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
+       // 测试 checkStatus 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCheckStatus(null)));
+       // 测试 returnTime 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setReturnTime(null)));
+       // 测试 remindDays 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindDays(null)));
+       // 测试 remindTime 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindTime(null)));
+       // 测试 customerId 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCustomerId(null)));
+       // 测试 contractId 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setContractId(null)));
+       // 测试 ownerUserId 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setOwnerUserId(null)));
+       // 测试 remark 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemark(null)));
+       // 测试 createTime 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
+       // 准备参数
+       ReceivablePlanExportReqVO reqVO = new ReceivablePlanExportReqVO();
+       reqVO.setIndexNo(null);
+       reqVO.setStatus(null);
+       reqVO.setCheckStatus(null);
+       reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setRemindDays(null);
+       reqVO.setRemindTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCustomerId(null);
+       reqVO.setContractId(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setRemark(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       List<ReceivablePlanDO> list = receivablePlanService.getReceivablePlanList(reqVO);
+       // 断言
+       assertEquals(1, list.size());
+       assertPojoEquals(dbReceivablePlan, list.get(0));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
index 97305a2e2..525391b33 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
@@ -3,3 +3,5 @@ DELETE FROM "crm_contract";
 DELETE FROM "crm_clue";
 
 DELETE FROM "crm_receivable";
+
+DELETE FROM "crm_receivable_plan";
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
index cdea50a96..8d98883b4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -73,3 +73,28 @@ CREATE TABLE IF NOT EXISTS "crm_receivable" (
     "deleted" bit NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
 ) COMMENT '回款管理';
+
+CREATE TABLE IF NOT EXISTS "crm_receivable_plan" (
+     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+     "index_no" bigint,
+     "receivable_id" bigint,
+     "status" int NOT NULL,
+     "check_status" varchar,
+     "process_instance_id" bigint,
+     "price" varchar,
+     "return_time" varchar,
+     "remind_days" bigint,
+     "remind_time" varchar,
+     "customer_id" bigint,
+     "contract_id" bigint,
+     "owner_user_id" bigint,
+     "sort" int,
+     "remark" varchar,
+     "creator" varchar DEFAULT '',
+     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+     "updater" varchar DEFAULT '',
+     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+     "deleted" bit NOT NULL DEFAULT FALSE,
+     PRIMARY KEY ("id")
+) COMMENT '回款计划';
+

From e4eeaa1d695887434caae1c0c6891424985c884d Mon Sep 17 00:00:00 2001
From: "913752709@qq.com" <913752709@qq.com>
Date: Sat, 21 Oct 2023 00:53:18 +0800
Subject: [PATCH 016/101] =?UTF-8?q?feat:=20crm=20=E7=BA=BF=E7=B4=A2?=
 =?UTF-8?q?=E8=A1=A8=E5=AE=8C=E5=96=84=20review=20=E6=8F=90=E5=88=B0?=
 =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             | 40 +++++++++----------
 .../common/validation/Telephone.java          | 28 +++++++++++++
 .../common/validation/TelephoneValidator.java | 25 ++++++++++++
 .../module/crm/enums/ErrorCodeConstants.java  |  4 +-
 .../admin/clue/vo/CrmClueBaseVO.java          | 27 ++++---------
 .../admin/clue/vo/CrmClueExcelVO.java         |  9 +++--
 .../admin/clue/vo/CrmCluePageReqVO.java       | 36 -----------------
 .../admin/clue/vo/CrmClueRespVO.java          |  8 ++++
 .../crm/dal/mysql/clue/CrmClueMapper.java     |  8 ----
 9 files changed, 96 insertions(+), 89 deletions(-)
 create mode 100644 yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java
 create mode 100644 yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 6c5594068..f21a9d4b3 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -41,23 +41,23 @@ CREATE TABLE `crm_contract`
 
 DROP TABLE IF EXISTS `crm_clue`;
 CREATE TABLE `crm_clue`  (
-     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
-     `transform_status` tinyint NOT NULL COMMENT '转化状态',
-     `follow_up_status` tinyint NOT NULL COMMENT '跟进状态',
-     `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '线索名称',
-     `customer_id` bigint NOT NULL COMMENT '客户id',
-     `contact_next_time` datetime NULL DEFAULT NULL COMMENT '下次联系时间',
-     `telephone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电话',
-     `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
-     `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地址',
-     `owner_user_id` bigint NULL DEFAULT NULL COMMENT '负责人的用户编号',
-     `contact_last_time` datetime NULL DEFAULT NULL COMMENT '最后跟进时间',
-     `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
-     `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-     `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-     `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-     `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-     `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-     `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-     PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '线索表' ROW_FORMAT = Dynamic;
\ No newline at end of file
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
+    `transform_status` tinyint DEFAULT NULL COMMENT '转化状态',
+    `follow_up_status` tinyint DEFAULT NULL COMMENT '跟进状态',
+    `name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '线索名称',
+    `customer_id` bigint NOT NULL COMMENT '客户id',
+    `contact_next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
+    `telephone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
+    `mobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
+    `address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
+    `owner_user_id` bigint NOT NULL COMMENT '负责人的用户编号',
+    `contact_last_time` datetime DEFAULT NULL COMMENT '最后跟进时间',
+    `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+    `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
+    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
+    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+    `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB COMMENT = '线索表' ;
\ No newline at end of file
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java
new file mode 100644
index 000000000..910601fd0
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = TelephoneValidator.class
+)
+public @interface Telephone {
+
+    String message() default "电话格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java
new file mode 100644
index 000000000..d214cfeef
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.PhoneUtil;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
+
+    @Override
+    public void initialize(Telephone annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (CharSequenceUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 3538b16ee..623c36842 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -12,7 +12,7 @@ public interface ErrorCodeConstants {
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
 
-    // TODO @wanwan:要单独一个分段噢
-    ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_000_001, "线索不存在");
+    // ========== 线索管理 1-020-001-000 ==========
+    ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
index 9a36c9175..a0efb99d6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
 
 import cn.iocoder.yudao.framework.common.validation.Mobile;
+import cn.iocoder.yudao.framework.common.validation.Telephone;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
+import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 import java.time.LocalDateTime;
 
@@ -17,44 +19,31 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class CrmClueBaseVO {
 
-    // TODO @wanwan:转化状态,新增和修改的时候,应该不传递的哈;而是在未来的时候,才会更新到
-    @Schema(description = "转化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
-    @NotNull(message = "转化状态不能为空")
-    private Boolean transformStatus;
-
-    // TODO @wanwan:同 transformStatus
-    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
-    @NotNull(message = "跟进状态不能为空")
-    private Boolean followUpStatus;
-
     @Schema(description = "线索名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "线索xxx")
-    @NotNull(message = "线索名称不能为空") // TODO @wanwan:应该是 NotEmpty 噢。空串都无法接受
+    @NotEmpty(message = "线索名称不能为空")
     private String name;
 
-    // TODO @wanwan:中英文之间,要有个空格;例如说,客户 id 不能为空
-    @Schema(description = "客户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "520")
-    @NotNull(message = "客户id不能为空")
+    @Schema(description = "客户 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "520")
+    @NotNull(message = "客户不能为空")
     private Long customerId;
 
     @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactNextTime;
 
-    // TODO @wanwan:@Schema 在 @Mobile 之前,要保持统一的顺序;2)可以加个 @Telephone 的校验格式;应该不是手机的格式哈
-    @Mobile(message = "电话格式不正确")
     @Schema(description = "电话", example = "18000000000")
+    @Telephone
     private String telephone;
 
-    // TODO @wanwan:@Schema 在 @Mobile 之前,要保持统一的顺序;2)类似 @Mobile 这个提示如果是默认的,就可以不写 message
-    @Mobile(message = "手机号格式不正确")
     @Schema(description = "手机号", example = "18000000000")
+    @Mobile
     private String mobile;
 
     @Schema(description = "地址", example = "北京市海淀区")
     private String address;
 
     @Schema(description = "负责人的用户编号", example = "27199")
-    // TODO @wanwan:这个是必填字段哈;
+    @NotNull(message = "负责人不能为空")
     private Long ownerUserId;
 
     @Schema(description = "最后跟进时间")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
index 13bc9c42b..d6457bd56 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
 
+import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import java.util.*;
@@ -12,7 +13,6 @@ import com.alibaba.excel.annotation.ExcelProperty;
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 
-
 /**
  * 线索 Excel VO
  *
@@ -21,20 +21,21 @@ import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 @Data
 public class CrmClueExcelVO {
 
-    @ExcelProperty("编号,主键自增")
+    @ExcelProperty("编号")
     private Long id;
 
     @ExcelProperty(value = "转化状态", converter = DictConvert.class)
-    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
     private Boolean transformStatus;
 
     @ExcelProperty(value = "跟进状态", converter = DictConvert.class)
-    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
     private Boolean followUpStatus;
 
     @ExcelProperty("线索名称")
     private String name;
 
+    // TODO 这里需要导出成客户名称
     @ExcelProperty("客户id")
     private Long customerId;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
index f4cba8fed..fa70be859 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
@@ -5,54 +5,18 @@ 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 CrmCluePageReqVO extends PageParam {
-
-    // TODO @wanwan:目前只要支持 name、mobile、telephone 的搜索即可;其它字段应该暂时不需要哈;
-
-    @Schema(description = "转化状态", example = "true")
-    private Boolean transformStatus;
-
-    @Schema(description = "跟进状态", example = "true")
-    private Boolean followUpStatus;
-
     @Schema(description = "线索名称", example = "线索xxx")
     private String name;
 
-    @Schema(description = "客户id", example = "520")
-    private Long customerId;
-
-    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] contactNextTime;
-
     @Schema(description = "电话", example = "18000000000")
     private String telephone;
 
     @Schema(description = "手机号", example = "18000000000")
     private String mobile;
-
-    @Schema(description = "地址", example = "北京市海淀区")
-    private String address;
-
-    @Schema(description = "负责人的用户编号", example = "27199")
-    private Long ownerUserId;
-
-    @Schema(description = "最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] contactLastTime;
-
-    @Schema(description = "创建时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] createTime;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
index 60eb2939b..6d2d30334 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
+
+import javax.validation.constraints.NotNull;
 import java.time.LocalDateTime;
 
 @Schema(description = "管理后台 - 线索 Response VO")
@@ -16,4 +18,10 @@ public class CrmClueRespVO extends CrmClueBaseVO {
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
+    @Schema(description = "转化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean followUpStatus;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
index 044615bcf..8437337f4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
@@ -19,17 +19,9 @@ public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
 
     default PageResult<CrmClueDO> selectPage(CrmCluePageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<CrmClueDO>()
-                .eqIfPresent(CrmClueDO::getTransformStatus, reqVO.getTransformStatus())
-                .eqIfPresent(CrmClueDO::getFollowUpStatus, reqVO.getFollowUpStatus())
                 .likeIfPresent(CrmClueDO::getName, reqVO.getName())
-                .eqIfPresent(CrmClueDO::getCustomerId, reqVO.getCustomerId())
-                .betweenIfPresent(CrmClueDO::getContactNextTime, reqVO.getContactNextTime())
                 .likeIfPresent(CrmClueDO::getTelephone, reqVO.getTelephone())
                 .likeIfPresent(CrmClueDO::getMobile, reqVO.getMobile())
-                .likeIfPresent(CrmClueDO::getAddress, reqVO.getAddress())
-                .eqIfPresent(CrmClueDO::getOwnerUserId, reqVO.getOwnerUserId())
-                .betweenIfPresent(CrmClueDO::getContactLastTime, reqVO.getContactLastTime())
-                .betweenIfPresent(CrmClueDO::getCreateTime, reqVO.getCreateTime())
                 .orderByDesc(CrmClueDO::getId));
     }
 

From 6f8fd383f7f6989616594966918f318c8d6f31fb Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 21 Oct 2023 21:35:35 +0800
Subject: [PATCH 017/101] =?UTF-8?q?code=20review=EF=BC=9ACRM=20=E5=95=86?=
 =?UTF-8?q?=E6=9C=BA=E7=9A=84=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/business/CrmBusinessController.java | 10 ----------
 .../admin/business/vo/CrmBusinessBaseVO.java  | 11 ++++++++++
 .../business/vo/CrmBusinessCreateReqVO.java   |  8 +++++---
 .../business/vo/CrmBusinessPageReqVO.java     |  2 ++
 .../business/vo/CrmBusinessUpdateReqVO.java   | 10 +++++++---
 .../convert/business/CrmBusinessConvert.java  |  2 --
 .../dataobject/business/CrmBusinessDO.java    | 20 +++++++++++++++++++
 7 files changed, 45 insertions(+), 18 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index 85cc48f79..b622f23c3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -19,7 +19,6 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.IOException;
-import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -67,15 +66,6 @@ public class CrmBusinessController {
         return success(CrmBusinessConvert.INSTANCE.convert(business));
     }
 
-    @GetMapping("/list")
-    @Operation(summary = "获得商机列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('crm:business:query')")
-    public CommonResult<List<CrmBusinessRespVO>> getBusinessList(@RequestParam("ids") Collection<Long> ids) {
-        List<CrmBusinessDO> list = businessService.getBusinessList(ids);
-        return success(CrmBusinessConvert.INSTANCE.convertList(list));
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得商机分页")
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
index 7e32feea0..061929ae6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
@@ -21,10 +21,12 @@ public class CrmBusinessBaseVO {
     @NotNull(message = "商机名称不能为空")
     private String name;
 
+    // TODO @ljileo:要写 requiredMode = Schema.RequiredMode.REQUIRED
     @Schema(description = "商机状态类型编号", example = "25714")
     @NotNull(message = "商机状态类型不能为空")
     private Long statusTypeId;
 
+    // TODO @ljileo:要写 requiredMode = Schema.RequiredMode.REQUIRED
     @Schema(description = "商机状态编号", example = "30320")
     @NotNull(message = "商机状态不能为空")
     private Long statusId;
@@ -41,9 +43,11 @@ public class CrmBusinessBaseVO {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime dealTime;
 
+    // TODO @ljileo:金额使用 Integer 类型,转换成分
     @Schema(description = "商机金额", example = "12371")
     private BigDecimal price;
 
+    // TODO @ljileo:折扣使用 Integer 类型,存储时,默认 * 100;展示的时候,前端需要 / 100;避免精度丢失问题
     @Schema(description = "整单折扣")
     private BigDecimal discountPercent;
 
@@ -53,25 +57,32 @@ public class CrmBusinessBaseVO {
     @Schema(description = "备注", example = "随便")
     private String remark;
 
+    // TODO @ljileo:这个新建和修改的时候,应该不传递哈;应该新建的人,就是它的负责人
     @Schema(description = "负责人的用户编号", example = "25562")
     private Long ownerUserId;
 
+    // TODO @ljileo:这个新建和修改的时候,应该不传递哈
     @Schema(description = "只读权限的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
     private String roUserIds;
 
+    // TODO @ljileo:这个新建和修改的时候,应该不传递哈
     @Schema(description = "读写权限的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
     private String rwUserIds;
 
+    // TODO @lijieo:新建的时候,不传递这个字段哈;
     @Schema(description = "1赢单2输单3无效", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Integer endStatus;
 
+    // TODO @lijieo:新建的时候,不传递这个字段哈;
     @Schema(description = "结束时的备注", example = "你说的对")
     private String endRemark;
 
+    // TODO @lijieo:新建的时候,不传递这个字段哈;
     @Schema(description = "最后跟进时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactLastTime;
 
+    // TODO @lijieo:新建的时候,不传递这个字段哈;
     @Schema(description = "跟进状态", example = "1")
     private Integer followUpStatus;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
index a03540cc6..f743c8469 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
@@ -1,9 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
 
-import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
 
 @Schema(description = "管理后台 - 商机创建 Request VO")
 @Data
@@ -11,4 +11,6 @@ import javax.validation.constraints.*;
 @ToString(callSuper = true)
 public class CrmBusinessCreateReqVO extends CrmBusinessBaseVO {
 
+    // TODO @ljileo:新建的时候,应该可以传递添加的产品;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
index 636661a9f..99a41d597 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
@@ -18,6 +18,8 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class CrmBusinessPageReqVO extends PageParam {
 
+    // TODO @ljileo:目前就使用 name 检索即可;
+
     @Schema(description = "商机名称", example = "李四")
     private String name;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
index 8922ec99b..f137d4c5b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 商机更新 Request VO")
 @Data
@@ -15,4 +17,6 @@ public class CrmBusinessUpdateReqVO extends CrmBusinessBaseVO {
     @NotNull(message = "主键不能为空")
     private Long id;
 
+    // TODO @ljileo:修改的时候,应该可以传递添加的产品;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index aa03ce0f0..72aad68de 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -25,8 +25,6 @@ public interface CrmBusinessConvert {
 
     CrmBusinessRespVO convert(CrmBusinessDO bean);
 
-    List<CrmBusinessRespVO> convertList(List<CrmBusinessDO> list);
-
     PageResult<CrmBusinessRespVO> convertPage(PageResult<CrmBusinessDO> page);
 
     List<CrmBusinessExcelVO> convertList02(List<CrmBusinessDO> list);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
index 620fb492e..795ddad39 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
@@ -35,10 +35,14 @@ public class CrmBusinessDO extends BaseDO {
     private String name;
     /**
      * 商机状态类型编号
+     *
+     *  TODO @ljileo:这个字段,后续要写下关联的实体哈
      */
     private Long statusTypeId;
     /**
      * 商机状态编号
+     *
+     * TODO @ljileo:这个字段,后续要写下关联的实体哈
      */
     private Long statusId;
     /**
@@ -47,6 +51,8 @@ public class CrmBusinessDO extends BaseDO {
     private LocalDateTime contactNextTime;
     /**
      * 客户编号
+     *
+     * TODO @ljileo:这个字段,后续要写下关联的实体哈
      */
     private Long customerId;
     /**
@@ -55,14 +61,20 @@ public class CrmBusinessDO extends BaseDO {
     private LocalDateTime dealTime;
     /**
      * 商机金额
+     *
+     * TODO @lijie:Integer
      */
     private BigDecimal price;
     /**
      * 整单折扣
+     *
+     * TODO @lijie:Integer
      */
     private BigDecimal discountPercent;
     /**
      * 产品总金额
+     *
+     * TODO @lijie:Integer
      */
     private BigDecimal productPrice;
     /**
@@ -75,14 +87,20 @@ public class CrmBusinessDO extends BaseDO {
     private Long ownerUserId;
     /**
      * 只读权限的用户编号数组
+     *
+     * TODO @lijie:应该是 List<Long>,然后使用下对应的 typehandler
      */
     private String roUserIds;
     /**
      * 读写权限的用户编号数组
+     *
+     * TODO @lijie:应该是 List<Long>,然后使用下对应的 typehandler
      */
     private String rwUserIds;
     /**
      * 1赢单2输单3无效
+     *
+     * TODO @lijie:搞个枚举;
      */
     private Integer endStatus;
     /**
@@ -95,6 +113,8 @@ public class CrmBusinessDO extends BaseDO {
     private LocalDateTime contactLastTime;
     /**
      * 跟进状态
+     *
+     * TODO @lijie:目前就是 Boolean;是否跟进
      */
     private Integer followUpStatus;
 

From f942e2c7f27529ddda2728e131559507b85fcc07 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 21 Oct 2023 21:55:21 +0800
Subject: [PATCH 018/101] =?UTF-8?q?code=20review=EF=BC=9ACRM=20=E5=9B=9E?=
 =?UTF-8?q?=E6=AC=BE=E8=AE=A1=E5=88=92=E7=9A=84=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../receivable/ReceivableController.java      | 13 +----
 .../receivable/ReceivablePlanController.java  | 47 +++++++------------
 .../admin/receivable/vo/ReceivableBaseVO.java | 20 +++++---
 .../receivable/vo/ReceivablePageReqVO.java    | 12 +++--
 .../receivable/vo/ReceivablePlanBaseVO.java   | 18 ++++---
 .../vo/ReceivablePlanPageReqVO.java           | 10 ++--
 .../dataobject/receivable/ReceivableDO.java   | 19 ++++++--
 .../receivable/ReceivablePlanServiceImpl.java | 38 ++++++++-------
 .../receivable/ReceivableServiceImpl.java     | 34 ++++++++------
 .../ReceivablePlanServiceImplTest.java        | 37 +++++++--------
 .../receivable/ReceivableServiceImplTest.java | 37 +++++++--------
 11 files changed, 147 insertions(+), 138 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
index 94c3a1c70..564e06bdb 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.*;
@@ -18,9 +19,8 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.IOException;
-import java.util.Collection;
 import java.util.List;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 
@@ -69,15 +69,6 @@ public class ReceivableController {
         return success(ReceivableConvert.INSTANCE.convert(receivable));
     }
 
-    @GetMapping("/list")
-    @Operation(summary = "获得回款管理列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
-    public CommonResult<List<ReceivableRespVO>> getReceivableList(@RequestParam("ids") Collection<Long> ids) {
-        List<ReceivableDO> list = receivableService.getReceivableList(ids);
-        return success(ReceivableConvert.INSTANCE.convertList(list));
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得回款管理分页")
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
index ae21a952d..9bf8d916a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
@@ -1,32 +1,28 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable;
 
-import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.security.access.prepost.PreAuthorize;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.Operation;
-
-import javax.validation.constraints.*;
-import javax.validation.*;
-import javax.servlet.http.*;
-import java.util.*;
-import java.io.IOException;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
-
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
 import cn.iocoder.yudao.module.crm.convert.receivable.ReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
 import cn.iocoder.yudao.module.crm.service.receivable.ReceivablePlanService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+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;
 
 /**
  * @author 赤焰
@@ -73,15 +69,6 @@ public class ReceivablePlanController {
         return success(ReceivablePlanConvert.INSTANCE.convert(receivablePlan));
     }
 
-    @GetMapping("/list")
-    @Operation(summary = "获得回款计划列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
-    public CommonResult<List<ReceivablePlanRespVO>> getReceivablePlanList(@RequestParam("ids") Collection<Long> ids) {
-        List<ReceivablePlanDO> list = receivablePlanService.getReceivablePlanList(ids);
-        return success(ReceivablePlanConvert.INSTANCE.convertList(list));
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得回款计划分页")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
index 4b63f1466..bb0bc5745 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
@@ -1,14 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import javax.validation.constraints.*;
-import org.springframework.format.annotation.DateTimeFormat;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
@@ -19,9 +17,13 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class ReceivableBaseVO {
 
+    // TODO @liuhongfeng:部分缺少 example 的字段,要补充下;
+    // TODO @liuhongfeng:部分字段,需要必传,要写 requiredMode = Schema.RequiredMode.REQUIRED,以及对应的 validator 非空校验
+
     @Schema(description = "回款编号")
     private String no;
 
+    // TODO @liuhongfeng:中英文之间,有个空格,这样更干净;
     @Schema(description = "回款计划ID", example = "31177")
     private Long planId;
 
@@ -31,9 +33,12 @@ public class ReceivableBaseVO {
     @Schema(description = "合同ID", example = "30305")
     private Long contractId;
 
+    // TODO @liuhongfeng:这个字段,可以写个枚举,然后 InEnum 去校验下;
+    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的
     @Schema(description = "审批状态", example = "1")
     private Integer checkStatus;
 
+    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
     @Schema(description = "工作流编号", example = "16568")
     private Long processInstanceId;
 
@@ -44,6 +49,7 @@ public class ReceivableBaseVO {
     @Schema(description = "回款方式", example = "2")
     private String returnType;
 
+    // TODO @liuhongfeng:使用 Int 哈,分;
     @Schema(description = "回款金额", example = "31859")
     private BigDecimal price;
 
@@ -56,12 +62,14 @@ public class ReceivableBaseVO {
     @Schema(description = "显示顺序")
     private Integer sort;
 
+    // TODO @芋艿:这个字段在看看;dataScope、dataScopeDeptIds
     @Schema(description = "数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
     private Integer dataScope;
 
     @Schema(description = "数据范围(指定部门数组)")
     private String dataScopeDeptIds;
 
+    // TODO @liuhongfeng:这个字段,这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
     @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     @NotNull(message = "状态不能为空")
     private Integer status;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
index 7b9e9df6d..7abd7cb39 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
@@ -1,12 +1,13 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
-import lombok.*;
+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.math.BigDecimal;
-import java.util.*;
-import io.swagger.v3.oas.annotations.media.Schema;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-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;
@@ -17,6 +18,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class ReceivablePageReqVO extends PageParam {
 
+    // TODO @liuhongfeng:目前就使用 no 检索即可;
     @Schema(description = "回款编号")
     private String no;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
index 638934a8e..59f333e4b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
@@ -1,15 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import javax.validation.constraints.*;
-import org.springframework.format.annotation.DateTimeFormat;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
@@ -20,9 +16,11 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class ReceivablePlanBaseVO {
 
+    // TODO 芋艿:这个字段,在想想命名;
     @Schema(description = "期数")
     private Long indexNo;
 
+    // TODO @liuhongfeng:中英文之间,有个空格,这样更干净;
     @Schema(description = "回款ID", example = "19852")
     private Long receivableId;
 
@@ -30,12 +28,16 @@ public class ReceivablePlanBaseVO {
     //@NotNull(message = "完成状态不能为空")
     private Integer status;
 
+    // TODO @liuhongfeng:这个字段,可以写个枚举,然后 InEnum 去校验下;
+    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的
     @Schema(description = "审批状态", example = "1")
     private String checkStatus;
 
+    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
     @Schema(description = "工作流编号", example = "8909")
     private Long processInstanceId;
 
+    // TODO @liuhongfeng:使用 Int 哈,分;
     @Schema(description = "计划回款金额", example = "29675")
     private BigDecimal price;
 
@@ -43,6 +45,7 @@ public class ReceivablePlanBaseVO {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime returnTime;
 
+    // TODO @liuhongfeng:这个字段,Integer
     @Schema(description = "提前几天提醒")
     private Long remindDays;
 
@@ -56,6 +59,7 @@ public class ReceivablePlanBaseVO {
     @Schema(description = "合同ID", example = "3473")
     private Long contractId;
 
+    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
     @Schema(description = "负责人", example = "17828")
     private Long ownerUserId;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
index 2c1166d1c..2d0e7d5ae 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
-import lombok.*;
-import java.util.*;
-import io.swagger.v3.oas.annotations.media.Schema;
 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;
@@ -15,6 +17,8 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class ReceivablePlanPageReqVO extends PageParam {
 
+    // TODO 芋艿:筛选字段,需要去掉几个,在想想;
+
     @Schema(description = "期数")
     private Long indexNo;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
index 561765b09..d80f76a91 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
@@ -1,13 +1,13 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;
 
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
+
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import com.baomidou.mybatisplus.annotation.*;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 
 /**
  * 回款管理 DO
@@ -35,14 +35,20 @@ public class ReceivableDO extends BaseDO {
     private String no;
     /**
      * 回款计划ID
+     *
+     * TODO @liuhongfeng:这个字段,后续要写下关联的实体哈
      */
     private Long planId;
     /**
      * 客户ID
+     *
+     * TODO @liuhongfeng:这个字段,后续要写下关联的实体哈
      */
     private Long customerId;
     /**
      * 合同ID
+     *
+     * TODO @liuhongfeng:这个字段,后续要写下关联的实体哈
      */
     private Long contractId;
     /**
@@ -53,6 +59,8 @@ public class ReceivableDO extends BaseDO {
     private Integer checkStatus;
     /**
      * 工作流编号
+     *
+     * TODO @liuhongfeng:这个字段,后续要写下关联的实体哈
      */
     private Long processInstanceId;
     /**
@@ -79,6 +87,7 @@ public class ReceivableDO extends BaseDO {
      * 显示顺序
      */
     private Integer sort;
+    // TODO 芋艿:dataScope、dataScopeDeptIds 在想下;
     /**
      * 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
index 1a1462388..c0b236ac0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
@@ -1,24 +1,26 @@
 package cn.iocoder.yudao.module.crm.service.receivable;
 
-import cn.hutool.core.util.ObjectUtil;
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-
-import java.util.*;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import cn.iocoder.yudao.module.crm.convert.receivable.ReceivablePlanConvert;
-import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-
 import cn.hutool.core.collection.CollUtil;
 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.module.crm.controller.admin.receivable.vo.ReceivablePlanCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.receivable.ReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.RECEIVABLE_PLAN_NOT_EXISTS;
 
 /**
  * 回款计划 Service 实现类
@@ -36,6 +38,7 @@ public class ReceivablePlanServiceImpl implements ReceivablePlanService {
     public Long createReceivablePlan(ReceivablePlanCreateReqVO createReqVO) {
         // 插入
         ReceivablePlanDO receivablePlan = ReceivablePlanConvert.INSTANCE.convert(createReqVO);
+        // TODO @liuhongfeng:空格要注释;if (ObjectUtil.isNull(receivablePlan.getStatus())) {
         if(ObjectUtil.isNull(receivablePlan.getStatus())){
             receivablePlan.setStatus(CommonStatusEnum.ENABLE.getStatus());
         }
@@ -48,6 +51,7 @@ public class ReceivablePlanServiceImpl implements ReceivablePlanService {
     public void updateReceivablePlan(ReceivablePlanUpdateReqVO updateReqVO) {
         // 校验存在
         validateReceivablePlanExists(updateReqVO.getId());
+
         // 更新
         ReceivablePlanDO updateObj = ReceivablePlanConvert.INSTANCE.convert(updateReqVO);
         receivablePlanMapper.updateById(updateObj);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
index 2f5abe1fc..94b25f6ed 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
@@ -1,21 +1,24 @@
 package cn.iocoder.yudao.module.crm.service.receivable;
 
-import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import java.util.*;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
-
-import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
-import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.RECEIVABLE_NOT_EXISTS;
 
 /**
  * 回款管理 Service 实现类
@@ -31,6 +34,8 @@ public class ReceivableServiceImpl implements ReceivableService {
 
     @Override
     public Long createReceivable(ReceivableCreateReqVO createReqVO) {
+        // TODO @liuhongfeng:planId 是否存在,是否合法,需要去校验;
+        // TODO @liuhongfeng:其它类似 customerId、contractId 也需要去校验;
         // 插入
         ReceivableDO receivable = ReceivableConvert.INSTANCE.convert(createReqVO);
         receivableMapper.insert(receivable);
@@ -42,6 +47,7 @@ public class ReceivableServiceImpl implements ReceivableService {
     public void updateReceivable(ReceivableUpdateReqVO updateReqVO) {
         // 校验存在
         validateReceivableExists(updateReqVO.getId());
+
         // 更新
         ReceivableDO updateObj = ReceivableConvert.INSTANCE.convert(updateReqVO);
         receivableMapper.updateById(updateObj);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
index 0d90fd1aa..3739481c0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
@@ -1,33 +1,30 @@
 package cn.iocoder.yudao.module.crm.service.receivable;
 
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
-import org.springframework.context.annotation.Import;
-import java.util.*;
-import java.time.LocalDateTime;
+import java.util.List;
 
-import static cn.hutool.core.util.RandomUtil.*;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
-import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.RECEIVABLE_PLAN_NOT_EXISTS;
 import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
 
+// TODO 芋艿:后续,需要补充测试用例
 /**
  * {@link ReceivablePlanServiceImpl} 的单元测试类
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
index 3d1b47403..b8d4018b4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
@@ -1,33 +1,30 @@
 package cn.iocoder.yudao.module.crm.service.receivable;
 
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
-import org.springframework.context.annotation.Import;
-import java.util.*;
-import java.time.LocalDateTime;
+import java.util.List;
 
-import static cn.hutool.core.util.RandomUtil.*;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
-import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.RECEIVABLE_NOT_EXISTS;
 import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
 
+// TODO 芋艿:等实现完,在校验下;
 /**
  * {@link ReceivableServiceImpl} 的单元测试类
  *

From b5560fa0be71cdbda972fdc2d14510638a7ef6a1 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 21 Oct 2023 22:05:37 +0800
Subject: [PATCH 019/101] =?UTF-8?q?code=20review=EF=BC=9ACRM=20=E7=BA=BF?=
 =?UTF-8?q?=E7=B4=A2=E8=A1=A8=E7=9A=84=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |  4 +--
 .../admin/clue/vo/CrmCluePageReqVO.java       |  2 ++
 .../admin/clue/vo/CrmClueUpdateReqVO.java     | 12 ++++---
 .../crm/convert/clue/CrmClueConvert.java      |  2 --
 .../crm/dal/dataobject/clue/CrmClueDO.java    |  9 ++---
 .../crm/service/clue/CrmClueServiceImpl.java  | 35 +++++++++++--------
 .../service/clue/CrmClueServiceImplTest.java  |  8 -----
 7 files changed, 34 insertions(+), 38 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 8a412a7e4..ace351d3b 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -15,8 +15,8 @@ public interface ErrorCodeConstants {
     // ========== 线索管理 1-020-001-000 ==========
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
 
-    // ========== 商机管理 1-020-001-000 ==========
-    ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_001_000, "商机不存在");
+    // ========== 商机管理 1-020-002-000 ==========
+    ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
 
     // TODO @liuhongfeng:错误码分段;
     ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_030_000_001, "回款管理不存在");
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
index fa70be859..4d28ebc73 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
@@ -11,6 +11,7 @@ import lombok.ToString;
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class CrmCluePageReqVO extends PageParam {
+
     @Schema(description = "线索名称", example = "线索xxx")
     private String name;
 
@@ -19,4 +20,5 @@ public class CrmCluePageReqVO extends PageParam {
 
     @Schema(description = "手机号", example = "18000000000")
     private String mobile;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
index 2a264de97..4526fbd2b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 线索更新 Request VO")
 @Data
@@ -11,8 +13,8 @@ import javax.validation.constraints.*;
 @ToString(callSuper = true)
 public class CrmClueUpdateReqVO extends CrmClueBaseVO {
 
-    @Schema(description = "编号,主键自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "10969")
-    @NotNull(message = "编号,主键自增不能为空")
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10969")
+    @NotNull(message = "编号不能为空")
     private Long id;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
index 144a77da6..76ea428c7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/clue/CrmClueConvert.java
@@ -25,8 +25,6 @@ public interface CrmClueConvert {
 
     CrmClueRespVO convert(CrmClueDO bean);
 
-    List<CrmClueRespVO> convertList(List<CrmClueDO> list);
-
     PageResult<CrmClueRespVO> convertPage(PageResult<CrmClueDO> page);
 
     List<CrmClueExcelVO> convertList02(List<CrmClueDO> list);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
index de30e1301..f9eee5b09 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
@@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
-import com.sun.xml.bind.v2.TODO;
 import lombok.*;
 
 import java.time.LocalDateTime;
@@ -32,14 +31,10 @@ public class CrmClueDO extends BaseDO {
     private Long id;
     /**
      * 转化状态
-     *
-     * 枚举 {@link TODO infra_boolean_string 对应的类}
      */
     private Boolean transformStatus;
     /**
      * 跟进状态
-     *
-     * 枚举 {@link TODO infra_boolean_string 对应的类}
      */
     private Boolean followUpStatus;
     /**
@@ -47,7 +42,9 @@ public class CrmClueDO extends BaseDO {
      */
     private String name;
     /**
-     * 客户id
+     * 客户 id
+     *
+     * // TODO @wanwan:要写下关联的实体,以及对应的属性哈
      */
     private Long customerId;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
index 92193361f..5e461ff87 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
@@ -1,22 +1,24 @@
 package cn.iocoder.yudao.module.crm.service.clue;
 
-import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-
-import java.util.*;
-import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
-import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CLUE_NOT_EXISTS;
 
 /**
  * 线索 Service 实现类
@@ -32,6 +34,7 @@ public class CrmClueServiceImpl implements CrmClueService {
 
     @Override
     public Long createClue(CrmClueCreateReqVO createReqVO) {
+        // TODO @wanwan:校验客户是否存在;以及类似的逻辑哈;如果目前还缺对应的模块的 service,可以先给自己写 todo;
         // 插入
         CrmClueDO clue = CrmClueConvert.INSTANCE.convert(createReqVO);
         clueMapper.insert(clue);
@@ -41,8 +44,10 @@ public class CrmClueServiceImpl implements CrmClueService {
 
     @Override
     public void updateClue(CrmClueUpdateReqVO updateReqVO) {
+        // TODO @wanwan:校验客户是否存在;以及类似的逻辑哈;如果目前还缺对应的模块的 service,可以先给自己写 todo;
         // 校验存在
         validateClueExists(updateReqVO.getId());
+
         // 更新
         CrmClueDO updateObj = CrmClueConvert.INSTANCE.convert(updateReqVO);
         clueMapper.updateById(updateObj);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
index 1051b2fcf..63a838164 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
@@ -144,17 +144,9 @@ public class CrmClueServiceImplTest extends BaseDbUnitTest {
        clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCreateTime(null)));
        // 准备参数
        CrmCluePageReqVO reqVO = new CrmCluePageReqVO();
-       reqVO.setTransformStatus(null);
-       reqVO.setFollowUpStatus(null);
        reqVO.setName(null);
-       reqVO.setCustomerId(null);
-       reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
        reqVO.setTelephone(null);
        reqVO.setMobile(null);
-       reqVO.setAddress(null);
-       reqVO.setOwnerUserId(null);
-       reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
 
        // 调用
        PageResult<CrmClueDO> pageResult = clueService.getCluePage(reqVO);

From 55f9e0131c85f27a65d3a2404866543630076f8d Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 21 Oct 2023 22:20:00 +0800
Subject: [PATCH 020/101] =?UTF-8?q?code=20review=EF=BC=9ACRM=20=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E8=81=94=E7=B3=BB=E4=BA=BA=E7=9A=84=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/contact/vo/ContactBaseVO.java       | 18 ++++++----
 .../admin/contact/vo/ContactPageReqVO.java    | 10 ++++--
 .../crm/dal/dataobject/contact/ContactDO.java | 18 ++++++----
 .../service/contact/ContactServiceImpl.java   | 35 +++++++++++--------
 4 files changed, 50 insertions(+), 31 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
index 5b39c1f41..0f9da5c06 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
@@ -1,14 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import javax.validation.constraints.*;
+import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
 /**
@@ -18,6 +16,8 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class ContactBaseVO {
 
+    // TODO @zyna:部分字段,缺少 example,需要补充;
+
     @Schema(description = "联系人名称", example = "张三")
     @NotNull(message = "姓名不能为空")
     private String name;
@@ -25,18 +25,22 @@ public class ContactBaseVO {
     @Schema(description = "下次联系时间")
     private LocalDateTime nextTime;
 
+    // TODO @zyna:缺少 validator 的校验
     @Schema(description = "手机号")
     private String mobile;
 
+    // TODO @zyna:缺少 validator 的校验
     @Schema(description = "电话")
     private String telephone;
 
+    // TODO @zyna:缺少 validator 的校验
     @Schema(description = "电子邮箱")
     private String email;
 
     @Schema(description = "职务")
     private String post;
 
+    // TODO @zyna:非空校验
     @Schema(description = "客户编号", example = "10795")
     private Long customerId;
 
@@ -46,10 +50,12 @@ public class ContactBaseVO {
     @Schema(description = "备注", example = "你说的对")
     private String remark;
 
+    // TODO @zyna:这个新建的时候,应该不会传递;而是后端默认设置自己为负责人;
     @Schema(description = "负责人用户编号", example = "7648")
     private Long ownerUserId;
 
     @Schema(description = "最后跟进时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime lastTime;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
index 058483926..31ad2a1fb 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
-import lombok.*;
-import java.util.*;
-import io.swagger.v3.oas.annotations.media.Schema;
 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;
@@ -15,6 +17,8 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class ContactPageReqVO extends PageParam {
 
+    // TODO @芋艿:需要查询的字段;
+
     @Schema(description = "联系人名称", example = "张三")
     private String name;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
index ee735ae61..2e7d2cfef 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
@@ -1,15 +1,15 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
 
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import com.baomidou.mybatisplus.annotation.*;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
 
 /**
- * crm联系人 DO
+ * crm 联系人 DO
  *
  * @author 芋道源码
  */
@@ -54,6 +54,8 @@ public class ContactDO extends BaseDO {
     private String post;
     /**
      * 客户编号
+     *
+     * TODO @zyna:关联的字段,也要写下
      */
     private Long customerId;
     /**
@@ -66,6 +68,8 @@ public class ContactDO extends BaseDO {
     private String remark;
     /**
      * 负责人用户编号
+     *
+     * TODO @zyna:关联的字段,也要写下
      */
     private Long ownerUserId;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index 85ce3c269..b21fee020 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -1,22 +1,24 @@
 package cn.iocoder.yudao.module.crm.service.contact;
 
-import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-
-import java.util.*;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
-import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTACT_NOT_EXISTS;
 
 /**
  * crm联系人 Service 实现类
@@ -32,6 +34,7 @@ public class ContactServiceImpl implements ContactService {
 
     @Override
     public Long createContact(ContactCreateReqVO createReqVO) {
+        // TODO @customerId:需要校验存在
         // 插入
         ContactDO contact = ContactConvert.INSTANCE.convert(createReqVO);
         contactMapper.insert(contact);
@@ -43,6 +46,8 @@ public class ContactServiceImpl implements ContactService {
     public void updateContact(ContactUpdateReqVO updateReqVO) {
         // 校验存在
         validateContactExists(updateReqVO.getId());
+        // TODO @customerId:需要校验存在
+
         // 更新
         ContactDO updateObj = ContactConvert.INSTANCE.convert(updateReqVO);
         contactMapper.updateById(updateObj);

From 918ffc40aec60f1481896c14a8e67f6df0a07d9e Mon Sep 17 00:00:00 2001
From: zyna <chenjidemenglin20@126.com>
Date: Sun, 22 Oct 2023 16:40:57 +0800
Subject: [PATCH 021/101] =?UTF-8?q?crm=E8=81=94=E7=B3=BB=E4=BA=BA=E5=89=8D?=
 =?UTF-8?q?=E7=AB=AFinit?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm_menu.sql                        |  55 +++
 yudao-ui-admin/src/api/crm/contact/contact.js |  54 +++
 .../src/views/crm/contact/index.vue           | 316 ++++++++++++++++++
 3 files changed, 425 insertions(+)
 create mode 100644 yudao-ui-admin/src/api/crm/contact/contact.js
 create mode 100644 yudao-ui-admin/src/views/crm/contact/index.vue

diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index d85c9496b..d3ef9418a 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -117,6 +117,61 @@ VALUES (
    '线索导出', 'crm:clue:export', 3, 5, @parentId,
    '', '', '', 0
 );
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+           '联系人', '', 2, 0, ${table.parentMenuId},
+           'contact', '', 'crm/contact/index', 0, 'Contact'
+       );
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '联系人查询', 'crm:contact:query', 3, 1, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '联系人创建', 'crm:contact:create', 3, 2, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '联系人更新', 'crm:contact:update', 3, 3, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '联系人删除', 'crm:contact:delete', 3, 4, @parentId,
+           '', '', '', 0
+       );
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+           '联系人导出', 'crm:contact:export', 3, 5, @parentId,
+           '', '', '', 0
+       );
 
 -- ----------------------------
 -- 合同菜单
diff --git a/yudao-ui-admin/src/api/crm/contact/contact.js b/yudao-ui-admin/src/api/crm/contact/contact.js
new file mode 100644
index 000000000..fd6e0c1ee
--- /dev/null
+++ b/yudao-ui-admin/src/api/crm/contact/contact.js
@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 创建crm联系人
+export function createContact(data) {
+  return request({
+    url: '/crm/contact/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新crm联系人
+export function updateContact(data) {
+  return request({
+    url: '/crm/contact/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除crm联系人
+export function deleteContact(id) {
+  return request({
+    url: '/crm/contact/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得crm联系人
+export function getContact(id) {
+  return request({
+    url: '/crm/contact/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得crm联系人分页
+export function getContactPage(query) {
+  return request({
+    url: '/crm/contact/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出crm联系人 Excel
+export function exportContactExcel(query) {
+  return request({
+    url: '/crm/contact/export-excel',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}
diff --git a/yudao-ui-admin/src/views/crm/contact/index.vue b/yudao-ui-admin/src/views/crm/contact/index.vue
new file mode 100644
index 000000000..acde3615d
--- /dev/null
+++ b/yudao-ui-admin/src/views/crm/contact/index.vue
@@ -0,0 +1,316 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="110px">
+      <el-form-item label="姓名" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入联系人名称" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="下次联系时间" prop="nextTime">
+        <el-date-picker v-model="queryParams.nextTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+      </el-form-item>
+      <el-form-item label="手机号" prop="mobile">
+        <el-input v-model="queryParams.mobile" placeholder="请输入手机号" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+<!--      <el-form-item label="电话" prop="telephone">-->
+<!--        <el-input v-model="queryParams.telephone" placeholder="请输入电话" clearable @keyup.enter.native="handleQuery"/>-->
+<!--      </el-form-item>-->
+<!--      <el-form-item label="电子邮箱" prop="email">-->
+<!--        <el-input v-model="queryParams.email" placeholder="请输入电子邮箱" clearable @keyup.enter.native="handleQuery"/>-->
+<!--      </el-form-item>-->
+<!--      <el-form-item label="职务" prop="post">-->
+<!--        <el-input v-model="queryParams.post" placeholder="请输入职务" clearable @keyup.enter.native="handleQuery"/>-->
+<!--      </el-form-item>-->
+      <el-form-item label="客户编号" prop="customerId">
+        <el-input v-model="queryParams.customerId" placeholder="请输入客户编号" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+<!--      <el-form-item label="地址" prop="address">-->
+<!--        <el-input v-model="queryParams.address" placeholder="请输入地址" clearable @keyup.enter.native="handleQuery"/>-->
+<!--      </el-form-item>-->
+<!--      <el-form-item label="备注" prop="remark">-->
+<!--        <el-input v-model="queryParams.remark" placeholder="请输入备注" clearable @keyup.enter.native="handleQuery"/>-->
+<!--      </el-form-item>-->
+      <el-form-item label="负责人用户编号" prop="ownerUserId">
+        <el-input v-model="queryParams.ownerUserId" placeholder="请输入负责人用户编号" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+<!--      <el-form-item label="创建时间" prop="createTime">-->
+<!--        <el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"-->
+<!--                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />-->
+<!--      </el-form-item>-->
+<!--      <el-form-item label="最后跟进时间" prop="lastTime">-->
+<!--        <el-date-picker v-model="queryParams.lastTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"-->
+<!--                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />-->
+<!--      </el-form-item>-->
+<!--      <el-form-item label="更新人" prop="updator">-->
+<!--        <el-input v-model="queryParams.updator" placeholder="请输入更新人" clearable @keyup.enter.native="handleQuery"/>-->
+<!--      </el-form-item>-->
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['crm:contact:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['crm:contact:export']">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="姓名" align="center" prop="name" />
+      <el-table-column label="下次联系时间" align="center" prop="nextTime" >
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.nextTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="手机号" align="center" prop="mobile" />
+      <el-table-column label="电话" align="center" prop="telephone" />
+      <el-table-column label="电子邮箱" align="center" prop="email" />
+      <el-table-column label="职务" align="center" prop="post" />
+      <el-table-column label="客户编号" align="center" prop="customerId" />
+      <el-table-column label="地址" align="center" prop="address" />
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="负责人用户编号" align="center" prop="ownerUserId" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="最后跟进时间" align="center" prop="lastTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.lastTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="更新人" align="center" prop="updator" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['crm:contact:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['crm:contact:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" v-dialogDrag append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
+        <el-form-item label="姓名" prop="name">
+          <el-input v-model="form.name" placeholder="请输入联系人名称" />
+        </el-form-item>
+        <el-form-item label="下次联系时间" prop="nextTime">
+          <el-date-picker clearable v-model="form.nextTime" type="date" value-format="timestamp" placeholder="选择下次联系时间" />
+        </el-form-item>
+        <el-form-item label="手机号" prop="mobile">
+          <el-input v-model="form.mobile" placeholder="请输入手机号" />
+        </el-form-item>
+        <el-form-item label="电话" prop="telephone">
+          <el-input v-model="form.telephone" placeholder="请输入电话" />
+        </el-form-item>
+        <el-form-item label="电子邮箱" prop="email">
+          <el-input v-model="form.email" placeholder="请输入电子邮箱" />
+        </el-form-item>
+        <el-form-item label="职务" prop="post">
+          <el-input v-model="form.post" placeholder="请输入职务" />
+        </el-form-item>
+        <el-form-item label="客户编号" prop="customerId">
+          <el-input v-model="form.customerId" placeholder="请输入客户编号" />
+        </el-form-item>
+        <el-form-item label="地址" prop="address">
+          <el-input v-model="form.address" placeholder="请输入地址" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入备注" />
+        </el-form-item>
+        <el-form-item label="负责人用户编号" prop="ownerUserId">
+          <el-input v-model="form.ownerUserId" placeholder="请输入负责人用户编号" />
+        </el-form-item>
+        <el-form-item label="最后跟进时间" prop="lastTime">
+          <el-date-picker clearable v-model="form.lastTime" type="date" value-format="timestamp" placeholder="选择最后跟进时间" />
+        </el-form-item>
+        <el-form-item label="更新人" prop="updator">
+          <el-input v-model="form.updator" placeholder="请输入更新人" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { createContact, updateContact, deleteContact, getContact, getContactPage, exportContactExcel } from "@/api/crm/contact/contact";
+
+export default {
+  name: "Contact",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // crm联系人列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        name: null,
+        nextTime: [],
+        mobile: null,
+        telephone: null,
+        email: null,
+        post: null,
+        customerId: null,
+        address: null,
+        remark: null,
+        ownerUserId: null,
+        createTime: [],
+        lastTime: [],
+        updator: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 执行查询
+      getContactPage(this.queryParams).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        name: undefined,
+        nextTime: undefined,
+        mobile: undefined,
+        telephone: undefined,
+        email: undefined,
+        post: undefined,
+        customerId: undefined,
+        address: undefined,
+        remark: undefined,
+        ownerUserId: undefined,
+        lastTime: undefined,
+        updator: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加联系人";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id;
+      getContact(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改联系人";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 修改的提交
+        if (this.form.id != null) {
+          updateContact(this.form).then(response => {
+            this.$modal.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          });
+          return;
+        }
+        // 添加的提交
+        createContact(this.form).then(response => {
+          this.$modal.msgSuccess("新增成功");
+          this.open = false;
+          this.getList();
+        });
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$modal.confirm('是否确认删除联系人编号为"' + id + '"的数据项?').then(function() {
+          return deleteContact(id);
+        }).then(() => {
+          this.getList();
+          this.$modal.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      // 处理查询参数
+      let params = {...this.queryParams};
+      params.pageNo = undefined;
+      params.pageSize = undefined;
+      this.$modal.confirm('是否确认导出所有联系人数据项?').then(() => {
+          this.exportLoading = true;
+          return exportContactExcel(params);
+        }).then(response => {
+          this.$download.excel(response, '联系人.xls');
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

From 04613af0cd9c7a547b6a9b3e838808963c672d3e Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Wed, 25 Oct 2023 00:25:00 +0800
Subject: [PATCH 022/101] =?UTF-8?q?feat:=20=E5=AE=A2=E6=88=B7=E8=A1=A8?=
 =?UTF-8?q?=E7=9A=84=20crud?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  32 ++++
 sql/mysql/crm_menu.sql                        |  60 +++++++
 .../module/crm/enums/ErrorCodeConstants.java  |   3 +
 .../admin/customer/CrmCustomerController.java |  81 ++++++++-
 .../admin/customer/vo/CrmCustomerBaseVO.java  |  72 ++++++++
 .../customer/vo/CrmCustomerCreateReqVO.java   |  20 +++
 .../admin/customer/vo/CrmCustomerExcelVO.java |  74 ++++++++
 .../customer/vo/CrmCustomerExportReqVO.java   |  22 +++
 .../customer/vo/CrmCustomerPageReqVO.java     |  27 +++
 .../admin/customer/vo/CrmCustomerRespVO.java  |  25 +++
 .../customer/vo/CrmCustomerUpdateReqVO.java   |  26 +++
 .../convert/customer/CrmCustomerConvert.java  |  34 ++++
 .../dataobject/customer/CrmCustomerDO.java    | 107 +++++++++++
 .../dal/mysql/customer/CrmCustomerMapper.java |  39 ++++
 .../service/customer/CrmCustomerService.java  |  75 ++++++++
 .../customer/CrmCustomerServiceImpl.java      |  88 +++++++++
 .../mapper/customer/CrmCustomerMapper.xml     |  12 ++
 .../customer/CrmCustomerServiceImplTest.java  | 170 ++++++++++++++++++
 .../src/test/resources/sql/clean.sql          |   2 +
 .../src/test/resources/sql/create_tables.sql  |  27 +++
 20 files changed, 989 insertions(+), 7 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 73cf99490..fc819787a 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -180,3 +180,35 @@ CREATE TABLE `crm_contact` (
                                `tenant_id` bigint DEFAULT NULL,
                                PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='crm联系人';
+
+-- ----------------------------
+-- 客户表
+-- ----------------------------
+CREATE TABLE `crm_customer` (
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
+    `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户名称',
+    `follow_up_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '跟进状态',
+    `lock_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '锁定状态',
+    `deal_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '成交状态',
+    `mobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机',
+    `telephone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
+    `website` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '网址',
+    `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+    `owner_user_id` bigint DEFAULT NULL COMMENT '负责人的用户编号',
+    `ro_user_ids` varchar(4096) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '只读权限的用户编号数组',
+    `rw_user_ids` varchar(4096) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '读写权限的用户编号数组',
+    `area_id` bigint DEFAULT NULL COMMENT '地区编号',
+    `detail_address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '详细地址',
+    `longitude` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地理位置经度',
+    `latitude` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地理位置维度',
+    `contact_last_time` datetime DEFAULT NULL COMMENT '最后跟进时间',
+    `contact_next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
+    `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
+    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
+    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+    `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
+    PRIMARY KEY (`id`),
+    KEY `owner_user_id` (`owner_user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户表';
\ No newline at end of file
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index d85c9496b..ec30b2dea 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -305,3 +305,63 @@ VALUES (
            '回款计划导出', 'crm:receivable-plan:export', 3, 5, @parentId,
            '', '', '', 0
        );
+
+-- ----------------------------
+-- 客户管理菜单
+-- ----------------------------
+
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+   '客户管理', '', 2, 0, 2375,
+   'customer', '', 'crm/customer/index', 0, 'CrmCustomer'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户查询', 'crm:customer:query', 3, 1, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户创建', 'crm:customer:create', 3, 2, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户更新', 'crm:customer:update', 3, 3, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户删除', 'crm:customer:delete', 3, 4, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户导出', 'crm:customer:export', 3, 5, @parentId,
+   '', '', '', 0
+);
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 07d9b6d6a..37443fbc8 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -26,4 +26,7 @@ public interface ErrorCodeConstants {
 
     ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_040_000_001, "回款计划不存在");
 
+    // ========== 客户管理 1_020_006_000 ==========
+    ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index b5aee837e..7c91b4c1a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -1,23 +1,90 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.customer.vo.*;
+import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+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 = "管理后台 - 客户信息")
+@Tag(name = "管理后台 - 客户")
 @RestController
 @RequestMapping("/crm/customer")
 @Validated
 public class CrmCustomerController {
 
-    @GetMapping("/test")
-    public CommonResult<String> test() {
-        return success("hello");
+    @Resource
+    private CrmCustomerService customerService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建客户")
+    @PreAuthorize("@ss.hasPermission('crm:customer:create')")
+    public CommonResult<Long> createCustomer(@Valid @RequestBody CrmCustomerCreateReqVO createReqVO) {
+        return success(customerService.createCustomer(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新客户")
+    @PreAuthorize("@ss.hasPermission('crm:customer:update')")
+    public CommonResult<Boolean> updateCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
+        customerService.updateCustomer(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除客户")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:customer:delete')")
+    public CommonResult<Boolean> deleteCustomer(@RequestParam("id") Long id) {
+        customerService.deleteCustomer(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得客户")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<CrmCustomerRespVO> getCustomer(@RequestParam("id") Long id) {
+        CrmCustomerDO customer = customerService.getCustomer(id);
+        return success(CrmCustomerConvert.INSTANCE.convert(customer));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得客户分页")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
+        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO);
+        return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出客户 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:customer:export')")
+    @OperateLog(type = EXPORT)
+    public void exportCustomerExcel(@Valid CrmCustomerExportReqVO exportReqVO,
+                                    HttpServletResponse response) throws IOException {
+        List<CrmCustomerDO> list = customerService.getCustomerList(exportReqVO);
+        // 导出 Excel
+        List<CrmCustomerExcelVO> datas = CrmCustomerConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "客户.xls", "数据", CrmCustomerExcelVO.class, datas);
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
new file mode 100644
index 000000000..039cda53a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import cn.iocoder.yudao.framework.common.validation.Mobile;
+import cn.iocoder.yudao.framework.common.validation.Telephone;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 客户 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmCustomerBaseVO {
+
+    @Schema(description = "客户名称", example = "赵六")
+    @NotEmpty(message = "客户名称不能为空")
+    private String name;
+
+    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "跟进状态不能为空")
+    private Boolean followUpStatus;
+
+    @Schema(description = "锁定状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "锁定状态不能为空")
+    private Boolean lockStatus;
+
+    @Schema(description = "手机", example = "18000000000")
+    @Mobile
+    private String mobile;
+
+    @Schema(description = "电话", example = "18000000000")
+    @Telephone
+    private String telephone;
+
+    @Schema(description = "网址", example = "https://www.baidu.com")
+    private String website;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "负责人的用户编号", example = "25682")
+    @NotNull(message = "负责人不能为空")
+    private Long ownerUserId;
+
+    @Schema(description = "地区编号", example = "20158")
+    private Long areaId;
+
+    @Schema(description = "详细地址", example = "北京市海淀区")
+    private String detailAddress;
+
+    @Schema(description = "地理位置经度", example = "116.40341")
+    private String longitude;
+
+    @Schema(description = "地理位置维度", example = "39.92409")
+    private String latitude;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactNextTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
new file mode 100644
index 000000000..dae9cf212
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 客户创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerCreateReqVO extends CrmCustomerBaseVO {
+
+    @Schema(description = "只读权限的用户编号数组")
+    private String roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private String rwUserIds;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
new file mode 100644
index 000000000..6cf7cbd0f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+
+/**
+ * 客户 Excel VO
+ *
+ * @author Wanwan
+ */
+@Data
+public class CrmCustomerExcelVO {
+
+    @ExcelProperty("编号")
+    private Long id;
+
+    @ExcelProperty("客户名称")
+    private String name;
+
+    @ExcelProperty(value = "跟进状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean followUpStatus;
+
+    @ExcelProperty(value = "锁定状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean lockStatus;
+
+    @ExcelProperty(value = "成交状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean dealStatus;
+
+    @ExcelProperty("手机")
+    private String mobile;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("网址")
+    private String website;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("地区编号")
+    private Long areaId;
+
+    @ExcelProperty("详细地址")
+    private String detailAddress;
+
+    @ExcelProperty("地理位置经度")
+    private String longitude;
+
+    @ExcelProperty("地理位置维度")
+    private String latitude;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
new file mode 100644
index 000000000..27198ff0d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 客户 Excel 导出 Request VO,参数和 CrmCustomerPageReqVO 是一致的")
+@Data
+public class CrmCustomerExportReqVO {
+
+    @Schema(description = "客户名称", example = "赵六")
+    private String name;
+
+    @Schema(description = "手机", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Schema(description = "网址", example = "https://www.baidu.com")
+    private String website;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
new file mode 100644
index 000000000..3871e422d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.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;
+
+@Schema(description = "管理后台 - 客户分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerPageReqVO extends PageParam {
+
+    @Schema(description = "客户名称", example = "赵六")
+    private String name;
+
+    @Schema(description = "手机", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Schema(description = "网址", example = "https://www.baidu.com")
+    private String website;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
new file mode 100644
index 000000000..b5c9850bc
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 客户 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerRespVO extends CrmCustomerBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    private Long id;
+
+    @Schema(description = "成交状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean dealStatus;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
new file mode 100644
index 000000000..a54938ea2
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 客户更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerUpdateReqVO extends CrmCustomerBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+    @Schema(description = "只读权限的用户编号数组")
+    private String roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private String rwUserIds;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
new file mode 100644
index 000000000..da683fbfb
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.customer;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+
+/**
+ * 客户 Convert
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmCustomerConvert {
+
+    CrmCustomerConvert INSTANCE = Mappers.getMapper(CrmCustomerConvert.class);
+
+    CrmCustomerDO convert(CrmCustomerCreateReqVO bean);
+
+    CrmCustomerDO convert(CrmCustomerUpdateReqVO bean);
+
+    CrmCustomerRespVO convert(CrmCustomerDO bean);
+
+    List<CrmCustomerRespVO> convertList(List<CrmCustomerDO> list);
+
+    PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> page);
+
+    List<CrmCustomerExcelVO> convertList02(List<CrmCustomerDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
new file mode 100644
index 000000000..d35650905
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -0,0 +1,107 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.customer;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.sun.xml.bind.v2.TODO;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 客户 DO
+ *
+ * @author Wanwan
+ */
+@TableName("crm_customer")
+@KeySequence("crm_customer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmCustomerDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 客户名称
+     */
+    private String name;
+    /**
+     * 跟进状态
+     * <p>
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean followUpStatus;
+    /**
+     * 锁定状态
+     * <p>
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean lockStatus;
+    /**
+     * 成交状态
+     * <p>
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean dealStatus;
+    /**
+     * 手机
+     */
+    private String mobile;
+    /**
+     * 电话
+     */
+    private String telephone;
+    /**
+     * 网址
+     */
+    private String website;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 负责人的用户编号
+     */
+    private Long ownerUserId;
+    /**
+     * 只读权限的用户编号数组
+     */
+    private String roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    private String rwUserIds;
+    /**
+     * 地区编号
+     */
+    private Long areaId;
+    /**
+     * 详细地址
+     */
+    private String detailAddress;
+    /**
+     * 地理位置经度
+     */
+    private String longitude;
+    /**
+     * 地理位置维度
+     */
+    private String latitude;
+    /**
+     * 最后跟进时间
+     */
+    private LocalDateTime contactLastTime;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime contactNextTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
new file mode 100644
index 000000000..647b6d812
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.customer;
+
+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.customer.vo.CrmCustomerExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 客户 Mapper
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
+
+    default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmCustomerDO>()
+                .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
+                .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
+                .eqIfPresent(CrmCustomerDO::getTelephone, reqVO.getTelephone())
+                .likeIfPresent(CrmCustomerDO::getWebsite, reqVO.getWebsite())
+                .orderByDesc(CrmCustomerDO::getId));
+    }
+
+    default List<CrmCustomerDO> selectList(CrmCustomerExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CrmCustomerDO>()
+                .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
+                .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
+                .eqIfPresent(CrmCustomerDO::getTelephone, reqVO.getTelephone())
+                .likeIfPresent(CrmCustomerDO::getWebsite, reqVO.getWebsite())
+                .orderByDesc(CrmCustomerDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
new file mode 100644
index 000000000..c2190ff94
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.crm.service.customer;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 客户 Service 接口
+ *
+ * @author Wanwan
+ */
+public interface CrmCustomerService {
+
+    /**
+     * 创建客户
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createCustomer(@Valid CrmCustomerCreateReqVO createReqVO);
+
+    /**
+     * 更新客户
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateCustomer(@Valid CrmCustomerUpdateReqVO updateReqVO);
+
+    /**
+     * 删除客户
+     *
+     * @param id 编号
+     */
+    void deleteCustomer(Long id);
+
+    /**
+     * 获得客户
+     *
+     * @param id 编号
+     * @return 客户
+     */
+    CrmCustomerDO getCustomer(Long id);
+
+    /**
+     * 获得客户列表
+     *
+     * @param ids 编号
+     * @return 客户列表
+     */
+    List<CrmCustomerDO> getCustomerList(Collection<Long> ids);
+
+    /**
+     * 获得客户分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 客户分页
+     */
+    PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO);
+
+    /**
+     * 获得客户列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 客户列表
+     */
+    List<CrmCustomerDO> getCustomerList(CrmCustomerExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
new file mode 100644
index 000000000..0bb224e7f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.crm.service.customer;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
+import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+
+/**
+ * 客户 Service 实现类
+ *
+ * @author Wanwan
+ */
+@Service
+@Validated
+public class CrmCustomerServiceImpl implements CrmCustomerService {
+
+    @Resource
+    private CrmCustomerMapper customerMapper;
+
+    @Override
+    public Long createCustomer(CrmCustomerCreateReqVO createReqVO) {
+        // 插入
+        CrmCustomerDO customer = CrmCustomerConvert.INSTANCE.convert(createReqVO);
+        customerMapper.insert(customer);
+        // 返回
+        return customer.getId();
+    }
+
+    @Override
+    public void updateCustomer(CrmCustomerUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateCustomerExists(updateReqVO.getId());
+        // 更新
+        CrmCustomerDO updateObj = CrmCustomerConvert.INSTANCE.convert(updateReqVO);
+        customerMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteCustomer(Long id) {
+        // 校验存在
+        validateCustomerExists(id);
+        // 删除
+        customerMapper.deleteById(id);
+    }
+
+    private void validateCustomerExists(Long id) {
+        if (customerMapper.selectById(id) == null) {
+            throw exception(CUSTOMER_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmCustomerDO getCustomer(Long id) {
+        return customerMapper.selectById(id);
+    }
+
+    @Override
+    public List<CrmCustomerDO> getCustomerList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return customerMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO) {
+        return customerMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CrmCustomerDO> getCustomerList(CrmCustomerExportReqVO exportReqVO) {
+        return customerMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerMapper.xml
new file mode 100644
index 000000000..85eb90d07
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
new file mode 100644
index 000000000..2e7d17907
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
@@ -0,0 +1,170 @@
+package cn.iocoder.yudao.module.crm.service.customer;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link CrmCustomerServiceImpl} 的单元测试类
+ *
+ * @author Wanwan
+ */
+@Import(CrmCustomerServiceImpl.class)
+public class CrmCustomerServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmCustomerServiceImpl customerService;
+
+    @Resource
+    private CrmCustomerMapper customerMapper;
+
+    @Test
+    public void testCreateCustomer_success() {
+        // 准备参数
+        CrmCustomerCreateReqVO reqVO = randomPojo(CrmCustomerCreateReqVO.class);
+
+        // 调用
+        Long customerId = customerService.createCustomer(reqVO);
+        // 断言
+        assertNotNull(customerId);
+        // 校验记录的属性是否正确
+        CrmCustomerDO customer = customerMapper.selectById(customerId);
+        assertPojoEquals(reqVO, customer);
+    }
+
+    @Test
+    public void testUpdateCustomer_success() {
+        // mock 数据
+        CrmCustomerDO dbCustomer = randomPojo(CrmCustomerDO.class);
+        customerMapper.insert(dbCustomer);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmCustomerUpdateReqVO reqVO = randomPojo(CrmCustomerUpdateReqVO.class, o -> {
+            o.setId(dbCustomer.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        customerService.updateCustomer(reqVO);
+        // 校验是否更新正确
+        CrmCustomerDO customer = customerMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, customer);
+    }
+
+    @Test
+    public void testUpdateCustomer_notExists() {
+        // 准备参数
+        CrmCustomerUpdateReqVO reqVO = randomPojo(CrmCustomerUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> customerService.updateCustomer(reqVO), CUSTOMER_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteCustomer_success() {
+        // mock 数据
+        CrmCustomerDO dbCustomer = randomPojo(CrmCustomerDO.class);
+        customerMapper.insert(dbCustomer);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbCustomer.getId();
+
+        // 调用
+        customerService.deleteCustomer(id);
+        // 校验数据不存在了
+        assertNull(customerMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteCustomer_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> customerService.deleteCustomer(id), CUSTOMER_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetCustomerPage() {
+        // mock 数据
+        CrmCustomerDO dbCustomer = randomPojo(CrmCustomerDO.class, o -> { // 等会查询到
+            o.setName(null);
+            o.setMobile(null);
+            o.setTelephone(null);
+            o.setWebsite(null);
+        });
+        customerMapper.insert(dbCustomer);
+        // 测试 name 不匹配
+        customerMapper.insert(cloneIgnoreId(dbCustomer, o -> o.setName(null)));
+        // 测试 mobile 不匹配
+        customerMapper.insert(cloneIgnoreId(dbCustomer, o -> o.setMobile(null)));
+        // 测试 telephone 不匹配
+        customerMapper.insert(cloneIgnoreId(dbCustomer, o -> o.setTelephone(null)));
+        // 测试 website 不匹配
+        customerMapper.insert(cloneIgnoreId(dbCustomer, o -> o.setWebsite(null)));
+        // 准备参数
+        CrmCustomerPageReqVO reqVO = new CrmCustomerPageReqVO();
+        reqVO.setName(null);
+        reqVO.setMobile(null);
+        reqVO.setTelephone(null);
+        reqVO.setWebsite(null);
+
+        // 调用
+        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbCustomer, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetCustomerList() {
+        // mock 数据
+        CrmCustomerDO dbCustomer = randomPojo(CrmCustomerDO.class, o -> { // 等会查询到
+            o.setName(null);
+            o.setMobile(null);
+            o.setTelephone(null);
+            o.setWebsite(null);
+        });
+        customerMapper.insert(dbCustomer);
+        // 测试 name 不匹配
+        customerMapper.insert(cloneIgnoreId(dbCustomer, o -> o.setName(null)));
+        // 测试 mobile 不匹配
+        customerMapper.insert(cloneIgnoreId(dbCustomer, o -> o.setMobile(null)));
+        // 测试 telephone 不匹配
+        customerMapper.insert(cloneIgnoreId(dbCustomer, o -> o.setTelephone(null)));
+        // 测试 website 不匹配
+        customerMapper.insert(cloneIgnoreId(dbCustomer, o -> o.setWebsite(null)));
+        // 准备参数
+        CrmCustomerExportReqVO reqVO = new CrmCustomerExportReqVO();
+        reqVO.setName(null);
+        reqVO.setMobile(null);
+        reqVO.setTelephone(null);
+        reqVO.setWebsite(null);
+
+        // 调用
+        List<CrmCustomerDO> list = customerService.getCustomerList(reqVO);
+        // 断言
+        assertEquals(1, list.size());
+        assertPojoEquals(dbCustomer, list.get(0));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
index 525391b33..c86e5a113 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
@@ -5,3 +5,5 @@ DELETE FROM "crm_clue";
 DELETE FROM "crm_receivable";
 
 DELETE FROM "crm_receivable_plan";
+
+DELETE FROM "crm_customer";
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
index 8d98883b4..f7d25f7ef 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -98,3 +98,30 @@ CREATE TABLE IF NOT EXISTS "crm_receivable_plan" (
      PRIMARY KEY ("id")
 ) COMMENT '回款计划';
 
+CREATE TABLE IF NOT EXISTS "crm_customer" (
+  "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+  "name" varchar,
+  "follow_up_status" bit NOT NULL,
+  "lock_status" bit NOT NULL,
+  "deal_status" bit NOT NULL,
+  "mobile" varchar,
+  "telephone" varchar,
+  "website" varchar,
+  "remark" varchar,
+  "owner_user_id" bigint,
+  "ro_user_ids" varchar,
+  "rw_user_ids" varchar,
+  "area_id" bigint,
+  "detail_address" varchar,
+  "longitude" varchar,
+  "latitude" varchar,
+  "contact_last_time" varchar,
+  "contact_next_time" varchar,
+  "creator" varchar DEFAULT '',
+  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "updater" varchar DEFAULT '',
+  "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  "deleted" bit NOT NULL DEFAULT FALSE,
+  "tenant_id" bigint NOT NULL,
+  PRIMARY KEY ("id")
+) COMMENT '客户表';
\ No newline at end of file

From 866eaedfeba5b3f6a038c106fc8e53285434b3d0 Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Wed, 25 Oct 2023 00:32:49 +0800
Subject: [PATCH 023/101] =?UTF-8?q?feat:=20=E5=AE=A2=E6=88=B7=E8=A1=A8?=
 =?UTF-8?q?=E7=9A=84=20crud?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../crm/dal/dataobject/clue/CrmClueDO.java    |  4 ++--
 .../crm/service/clue/CrmClueServiceImpl.java  | 21 +++++++++++++++++--
 2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
index f9eee5b09..067bec65c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.clue;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -43,8 +44,7 @@ public class CrmClueDO extends BaseDO {
     private String name;
     /**
      * 客户 id
-     *
-     * // TODO @wanwan:要写下关联的实体,以及对应的属性哈
+     * 对应 {@link CrmCustomerDO#getId()}
      */
     private Long customerId;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
index 5e461ff87..3389c4b26 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -19,6 +20,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.CLUE_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
 
 /**
  * 线索 Service 实现类
@@ -31,10 +33,13 @@ public class CrmClueServiceImpl implements CrmClueService {
 
     @Resource
     private CrmClueMapper clueMapper;
+    @Resource
+    private CrmCustomerService customerService;
 
     @Override
     public Long createClue(CrmClueCreateReqVO createReqVO) {
-        // TODO @wanwan:校验客户是否存在;以及类似的逻辑哈;如果目前还缺对应的模块的 service,可以先给自己写 todo;
+        // 校验客户是否存在
+        validateCustomerExists(createReqVO.getCustomerId());
         // 插入
         CrmClueDO clue = CrmClueConvert.INSTANCE.convert(createReqVO);
         clueMapper.insert(clue);
@@ -44,9 +49,10 @@ public class CrmClueServiceImpl implements CrmClueService {
 
     @Override
     public void updateClue(CrmClueUpdateReqVO updateReqVO) {
-        // TODO @wanwan:校验客户是否存在;以及类似的逻辑哈;如果目前还缺对应的模块的 service,可以先给自己写 todo;
         // 校验存在
         validateClueExists(updateReqVO.getId());
+        // 校验客户是否存在
+        validateCustomerExists(updateReqVO.getCustomerId());
 
         // 更新
         CrmClueDO updateObj = CrmClueConvert.INSTANCE.convert(updateReqVO);
@@ -90,4 +96,15 @@ public class CrmClueServiceImpl implements CrmClueService {
         return clueMapper.selectList(exportReqVO);
     }
 
+    /**
+     * 校验客户是否存在
+     *
+     * @param customerId 客户id
+     */
+    private void validateCustomerExists(Long customerId) {
+        if (customerService.getCustomer(customerId) == null) {
+            throw exception(CUSTOMER_NOT_EXISTS);
+        }
+    }
+
 }

From 75115bcc488053b339c010f5b4af834ac2f61442 Mon Sep 17 00:00:00 2001
From: ljlleo <e0101561@ceic.com>
Date: Thu, 26 Oct 2023 16:22:44 +0800
Subject: [PATCH 024/101] =?UTF-8?q?1=E3=80=81=E5=95=86=E6=9C=BA=E5=89=8D?=
 =?UTF-8?q?=E7=AB=AF=E9=A1=B5=E9=9D=A2=EF=BC=9B=202=E3=80=81=E5=95=86?=
 =?UTF-8?q?=E6=9C=BA=E7=8A=B6=E6=80=81=E7=B1=BB=E5=9E=8B=E5=92=8C=E5=95=86?=
 =?UTF-8?q?=E6=9C=BA=E7=8A=B6=E6=80=81=E5=AD=97=E6=AE=B5=E7=9A=84=E5=A4=84?=
 =?UTF-8?q?=E7=90=86=EF=BC=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  34 ++
 .../module/crm/enums/ErrorCodeConstants.java  |   2 +
 .../admin/business/CrmBusinessController.java |   1 +
 .../admin/business/vo/CrmBusinessBaseVO.java  |  40 +--
 .../business/vo/CrmBusinessPageReqVO.java     |  63 ----
 .../CrmBusinessStatusController.java          | 115 ++++++
 .../vo/CrmBusinessStatusBaseVO.java           |  29 ++
 .../vo/CrmBusinessStatusCreateReqVO.java      |  14 +
 .../vo/CrmBusinessStatusExcelVO.java          |  32 ++
 .../vo/CrmBusinessStatusExportReqVO.java      |  24 ++
 .../vo/CrmBusinessStatusPageReqVO.java        |  18 +
 .../vo/CrmBusinessStatusRespVO.java           |  15 +
 .../vo/CrmBusinessStatusUpdateReqVO.java      |  18 +
 .../CrmBusinessStatusTypeController.java      | 108 ++++++
 .../vo/CrmBusinessStatusTypeBaseVO.java       |  27 ++
 .../vo/CrmBusinessStatusTypeCreateReqVO.java  |  14 +
 .../vo/CrmBusinessStatusTypeExcelVO.java      |  34 ++
 .../vo/CrmBusinessStatusTypeExportReqVO.java  |  29 ++
 .../vo/CrmBusinessStatusTypePageReqVO.java    |  21 ++
 .../vo/CrmBusinessStatusTypeRespVO.java       |  19 +
 .../vo/CrmBusinessStatusTypeUpdateReqVO.java  |  18 +
 .../CrmBusinessStatusConvert.java             |  34 ++
 .../CrmBusinessStatusTypeConvert.java         |  34 ++
 .../dataobject/business/CrmBusinessDO.java    |  16 +-
 .../businessstatus/CrmBusinessStatusDO.java   |  44 +++
 .../CrmBusinessStatusTypeDO.java              |  40 +++
 .../dal/mysql/business/CrmBusinessMapper.java |  17 -
 .../CrmBusinessStatusMapper.java              |  40 +++
 .../CrmBusinessStatusTypeMapper.java          |  43 +++
 .../CrmBusinessStatusService.java             |  90 +++++
 .../CrmBusinessStatusServiceImpl.java         |  99 ++++++
 .../CrmBusinessStatusTypeService.java         |  83 +++++
 .../CrmBusinessStatusTypeServiceImpl.java     |  95 +++++
 .../CrmBusinessStatusTypeMapper.xml           |  12 +
 .../CrmBusinessStatusServiceImplTest.java     | 170 +++++++++
 .../CrmBusinessStatusTypeServiceImplTest.java | 171 +++++++++
 yudao-ui-admin/src/api/crm/business.js        |  54 +++
 yudao-ui-admin/src/api/crm/businessStatus.js  |  70 ++++
 .../src/api/crm/businessStatusType.js         |  62 ++++
 .../src/views/crm/business/index.vue          | 335 ++++++++++++++++++
 40 files changed, 2059 insertions(+), 125 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusPageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypePageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/businessstatus/CrmBusinessStatusConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/businessstatustype/CrmBusinessStatusTypeConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatus/CrmBusinessStatusDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatustype/CrmBusinessStatusTypeDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatus/CrmBusinessStatusMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatustype/CrmBusinessStatusTypeMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/businessstatustype/CrmBusinessStatusTypeMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImplTest.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImplTest.java
 create mode 100644 yudao-ui-admin/src/api/crm/business.js
 create mode 100644 yudao-ui-admin/src/api/crm/businessStatus.js
 create mode 100644 yudao-ui-admin/src/api/crm/businessStatusType.js
 create mode 100644 yudao-ui-admin/src/views/crm/business/index.vue

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 73cf99490..0cff91996 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -62,6 +62,10 @@ CREATE TABLE `crm_clue`  (
                              PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB COMMENT = '线索表' ;
 
+-- ----------------------------
+-- 商机表
+-- ----------------------------
+
 DROP TABLE IF EXISTS `crm_business`;
 CREATE TABLE `crm_business` (
                                 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
@@ -180,3 +184,33 @@ CREATE TABLE `crm_contact` (
                                `tenant_id` bigint DEFAULT NULL,
                                PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='crm联系人';
+
+-- ----------------------------
+-- 商机状态表
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_business_status`;
+CREATE TABLE `crm_business_status` (
+                                       `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                                       `type_id` bigint NOT NULL COMMENT '状态类型编号',
+                                       `name` varchar(100) NOT NULL COMMENT '状态名',
+                                       `percent` varchar(20) DEFAULT NULL COMMENT '赢单率',
+                                       `sort` int DEFAULT NULL COMMENT '排序',
+                                       `tenant_id` bigint DEFAULT '1' COMMENT '租户ID',
+                                       PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商机状态表'
+
+-- ----------------------------
+-- 商机状态类型表
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_business_status_type`;
+CREATE TABLE `crm_business_status_type` (
+                                            `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                                            `name` varchar(100) NOT NULL COMMENT '状态类型名',
+                                            `dept_ids` varchar(200) NOT NULL COMMENT '使用的部门编号',
+                                            `status` bit(1) NOT NULL DEFAULT b'0' COMMENT '开启状态',
+                                            `creator` varchar(64) NOT NULL COMMENT '创建人',
+                                            `create_time` datetime NOT NULL COMMENT '创建时间',
+                                            `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                                            `tenant_id` bigint DEFAULT '1' COMMENT '租户ID',
+                                            PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商机状态类型表'
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 07d9b6d6a..e7031ff13 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -17,6 +17,8 @@ public interface ErrorCodeConstants {
 
     // ========== 商机管理 1-020-002-000 ==========
     ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
+    ErrorCode BUSINESS_STATUS_TYPE_NOT_EXISTS = new ErrorCode(1_020_002_001, "商机状态类型不存在");
+    ErrorCode BUSINESS_STATUS_NOT_EXISTS = new ErrorCode(1_020_002_002, "商机状态不存在");
 
     // ========== 联系人管理 1-020-003-000 ==========
     ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index b622f23c3..d517a9f91 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -71,6 +71,7 @@ public class CrmBusinessController {
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
     public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPage(@Valid CrmBusinessPageReqVO pageVO) {
         PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO);
+
         return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
index 061929ae6..9bcffb117 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
@@ -21,13 +21,11 @@ public class CrmBusinessBaseVO {
     @NotNull(message = "商机名称不能为空")
     private String name;
 
-    // TODO @ljileo:要写 requiredMode = Schema.RequiredMode.REQUIRED
-    @Schema(description = "商机状态类型编号", example = "25714")
+    @Schema(description = "商机状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714")
     @NotNull(message = "商机状态类型不能为空")
     private Long statusTypeId;
 
-    // TODO @ljileo:要写 requiredMode = Schema.RequiredMode.REQUIRED
-    @Schema(description = "商机状态编号", example = "30320")
+    @Schema(description = "商机状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30320")
     @NotNull(message = "商机状态不能为空")
     private Long statusId;
 
@@ -43,13 +41,12 @@ public class CrmBusinessBaseVO {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime dealTime;
 
-    // TODO @ljileo:金额使用 Integer 类型,转换成分
     @Schema(description = "商机金额", example = "12371")
-    private BigDecimal price;
+    private Integer price;
 
     // TODO @ljileo:折扣使用 Integer 类型,存储时,默认 * 100;展示的时候,前端需要 / 100;避免精度丢失问题
     @Schema(description = "整单折扣")
-    private BigDecimal discountPercent;
+    private Integer discountPercent;
 
     @Schema(description = "产品总金额", example = "12025")
     private BigDecimal productPrice;
@@ -57,33 +54,4 @@ public class CrmBusinessBaseVO {
     @Schema(description = "备注", example = "随便")
     private String remark;
 
-    // TODO @ljileo:这个新建和修改的时候,应该不传递哈;应该新建的人,就是它的负责人
-    @Schema(description = "负责人的用户编号", example = "25562")
-    private Long ownerUserId;
-
-    // TODO @ljileo:这个新建和修改的时候,应该不传递哈
-    @Schema(description = "只读权限的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
-    private String roUserIds;
-
-    // TODO @ljileo:这个新建和修改的时候,应该不传递哈
-    @Schema(description = "读写权限的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
-    private String rwUserIds;
-
-    // TODO @lijieo:新建的时候,不传递这个字段哈;
-    @Schema(description = "1赢单2输单3无效", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    private Integer endStatus;
-
-    // TODO @lijieo:新建的时候,不传递这个字段哈;
-    @Schema(description = "结束时的备注", example = "你说的对")
-    private String endRemark;
-
-    // TODO @lijieo:新建的时候,不传递这个字段哈;
-    @Schema(description = "最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime contactLastTime;
-
-    // TODO @lijieo:新建的时候,不传递这个字段哈;
-    @Schema(description = "跟进状态", example = "1")
-    private Integer followUpStatus;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
index 99a41d597..c8368cce7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
@@ -5,12 +5,6 @@ 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.math.BigDecimal;
-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
@@ -18,64 +12,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class CrmBusinessPageReqVO extends PageParam {
 
-    // TODO @ljileo:目前就使用 name 检索即可;
-
     @Schema(description = "商机名称", example = "李四")
     private String name;
 
-    @Schema(description = "商机状态类型编号", example = "25714")
-    private Long statusTypeId;
-
-    @Schema(description = "商机状态编号", example = "30320")
-    private Long statusId;
-
-    @Schema(description = "下次联系时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] contactNextTime;
-
-    @Schema(description = "客户编号", example = "10299")
-    private Long customerId;
-
-    @Schema(description = "预计成交日期")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] dealTime;
-
-    @Schema(description = "商机金额", example = "12371")
-    private BigDecimal price;
-
-    @Schema(description = "整单折扣")
-    private BigDecimal discountPercent;
-
-    @Schema(description = "产品总金额", example = "12025")
-    private BigDecimal productPrice;
-
-    @Schema(description = "备注", example = "随便")
-    private String remark;
-
-    @Schema(description = "负责人的用户编号", example = "25562")
-    private Long ownerUserId;
-
-    @Schema(description = "创建时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] createTime;
-
-    @Schema(description = "只读权限的用户编号数组")
-    private String roUserIds;
-
-    @Schema(description = "读写权限的用户编号数组")
-    private String rwUserIds;
-
-    @Schema(description = "1赢单2输单3无效", example = "1")
-    private Integer endStatus;
-
-    @Schema(description = "结束时的备注", example = "你说的对")
-    private String endRemark;
-
-    @Schema(description = "最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] contactLastTime;
-
-    @Schema(description = "跟进状态", example = "1")
-    private Integer followUpStatus;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java
new file mode 100644
index 000000000..fddec8d8d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java
@@ -0,0 +1,115 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.businessstatus.vo.*;
+import cn.iocoder.yudao.module.crm.convert.businessstatus.CrmBusinessStatusConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
+import cn.iocoder.yudao.module.crm.service.businessstatus.CrmBusinessStatusService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+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/business-status")
+@Validated
+public class CrmBusinessStatusController {
+
+    @Resource
+    private CrmBusinessStatusService businessStatusService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建商机状态")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:create')")
+    public CommonResult<Long> createBusinessStatus(@Valid @RequestBody CrmBusinessStatusCreateReqVO createReqVO) {
+        return success(businessStatusService.createBusinessStatus(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新商机状态")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:update')")
+    public CommonResult<Boolean> updateBusinessStatus(@Valid @RequestBody CrmBusinessStatusUpdateReqVO updateReqVO) {
+        businessStatusService.updateBusinessStatus(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除商机状态")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:business-status:delete')")
+    public CommonResult<Boolean> deleteBusinessStatus(@RequestParam("id") Long id) {
+        businessStatusService.deleteBusinessStatus(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得商机状态")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<CrmBusinessStatusRespVO> getBusinessStatus(@RequestParam("id") Long id) {
+        CrmBusinessStatusDO businessStatus = businessStatusService.getBusinessStatus(id);
+        return success(CrmBusinessStatusConvert.INSTANCE.convert(businessStatus));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得商机状态列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusList(@RequestParam("ids") Collection<Long> ids) {
+        List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusList(ids);
+        return success(CrmBusinessStatusConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商机状态分页")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<PageResult<CrmBusinessStatusRespVO>> getBusinessStatusPage(@Valid CrmBusinessStatusPageReqVO pageVO) {
+        PageResult<CrmBusinessStatusDO> pageResult = businessStatusService.getBusinessStatusPage(pageVO);
+        return success(CrmBusinessStatusConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出商机状态 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:export')")
+    @OperateLog(type = EXPORT)
+    public void exportBusinessStatusExcel(@Valid CrmBusinessStatusExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusList(exportReqVO);
+        // 导出 Excel
+        List<CrmBusinessStatusExcelVO> datas = CrmBusinessStatusConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "商机状态.xls", "数据", CrmBusinessStatusExcelVO.class, datas);
+    }
+
+    @GetMapping("/get-simple-list")
+    @Operation(summary = "获得商机状态列表")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusListByTypeId(@RequestParam("typeId") Integer typeId) {
+        List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusListByTypeId(typeId);
+        return success(CrmBusinessStatusConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/get-all-list")
+    @Operation(summary = "获得商机状态列表")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusList() {
+        List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusList();
+        return success(CrmBusinessStatusConvert.INSTANCE.convertList(list));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java
new file mode 100644
index 000000000..eec9de8b0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 商机状态 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmBusinessStatusBaseVO {
+
+    @Schema(description = "状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22882")
+    @NotNull(message = "状态类型编号不能为空")
+    private Long typeId;
+
+    @Schema(description = "状态名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotNull(message = "状态名不能为空")
+    private String name;
+
+    @Schema(description = "赢单率")
+    private String percent;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusCreateReqVO.java
new file mode 100644
index 000000000..04e999474
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 商机状态创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusCreateReqVO extends CrmBusinessStatusBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java
new file mode 100644
index 000000000..83002469e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * 商机状态 Excel VO
+ *
+ * @author ljlleo
+ */
+@Data
+public class CrmBusinessStatusExcelVO {
+
+    @ExcelProperty("主键")
+    private Long id;
+
+    @ExcelProperty("状态类型编号")
+    private Long typeId;
+
+    @ExcelProperty("状态名")
+    private String name;
+
+    @ExcelProperty("赢单率")
+    private String percent;
+
+    @ExcelProperty("排序")
+    private Integer sort;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java
new file mode 100644
index 000000000..3fecb03ee
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+@Schema(description = "管理后台 - 商机状态 Excel 导出 Request VO,参数和 CrmBusinessStatusPageReqVO 是一致的")
+@Data
+public class CrmBusinessStatusExportReqVO {
+
+    @Schema(description = "状态类型编号", example = "22882")
+    private Long typeId;
+
+    @Schema(description = "状态名", example = "李四")
+    private String name;
+
+    @Schema(description = "赢单率")
+    private String percent;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusPageReqVO.java
new file mode 100644
index 000000000..af03512af
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusPageReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.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;
+
+@Schema(description = "管理后台 - 商机状态分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusPageReqVO extends PageParam {
+
+    @Schema(description = "状态类型编号", example = "22882")
+    private Long typeId;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java
new file mode 100644
index 000000000..2a0fa07a4
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+@Schema(description = "管理后台 - 商机状态 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusRespVO extends CrmBusinessStatusBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "6802")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java
new file mode 100644
index 000000000..11f403330
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 商机状态更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusUpdateReqVO extends CrmBusinessStatusBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "6802")
+    @NotNull(message = "主键不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java
new file mode 100644
index 000000000..ae0c55d62
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java
@@ -0,0 +1,108 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.businessstatustype.vo.*;
+import cn.iocoder.yudao.module.crm.convert.businessstatustype.CrmBusinessStatusTypeConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
+import cn.iocoder.yudao.module.crm.service.businessstatustype.CrmBusinessStatusTypeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+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/business-status-type")
+@Validated
+public class CrmBusinessStatusTypeController {
+
+    @Resource
+    private CrmBusinessStatusTypeService businessStatusTypeService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建商机状态类型")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:create')")
+    public CommonResult<Long> createBusinessStatusType(@Valid @RequestBody CrmBusinessStatusTypeCreateReqVO createReqVO) {
+        return success(businessStatusTypeService.createBusinessStatusType(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新商机状态类型")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:update')")
+    public CommonResult<Boolean> updateBusinessStatusType(@Valid @RequestBody CrmBusinessStatusTypeUpdateReqVO updateReqVO) {
+        businessStatusTypeService.updateBusinessStatusType(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除商机状态类型")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:delete')")
+    public CommonResult<Boolean> deleteBusinessStatusType(@RequestParam("id") Long id) {
+        businessStatusTypeService.deleteBusinessStatusType(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得商机状态类型")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
+    public CommonResult<CrmBusinessStatusTypeRespVO> getBusinessStatusType(@RequestParam("id") Long id) {
+        CrmBusinessStatusTypeDO businessStatusType = businessStatusTypeService.getBusinessStatusType(id);
+        return success(CrmBusinessStatusTypeConvert.INSTANCE.convert(businessStatusType));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得商机状态类型列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
+    public CommonResult<List<CrmBusinessStatusTypeRespVO>> getBusinessStatusTypeList(@RequestParam("ids") Collection<Long> ids) {
+        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeList(ids);
+        return success(CrmBusinessStatusTypeConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商机状态类型分页")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
+    public CommonResult<PageResult<CrmBusinessStatusTypeRespVO>> getBusinessStatusTypePage(@Valid CrmBusinessStatusTypePageReqVO pageVO) {
+        PageResult<CrmBusinessStatusTypeDO> pageResult = businessStatusTypeService.getBusinessStatusTypePage(pageVO);
+        return success(CrmBusinessStatusTypeConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出商机状态类型 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:export')")
+    @OperateLog(type = EXPORT)
+    public void exportBusinessStatusTypeExcel(@Valid CrmBusinessStatusTypeExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeList(exportReqVO);
+        // 导出 Excel
+        List<CrmBusinessStatusTypeExcelVO> datas = CrmBusinessStatusTypeConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "商机状态类型.xls", "数据", CrmBusinessStatusTypeExcelVO.class, datas);
+    }
+
+    @GetMapping("/get-simple-list")
+    @Operation(summary = "获得商机状态类型列表")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
+    public CommonResult<List<CrmBusinessStatusTypeRespVO>> getBusinessStatusTypeList() {
+        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(CrmBusinessStatusTypeConvert.INSTANCE.convertList(list));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeBaseVO.java
new file mode 100644
index 000000000..c472a3471
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeBaseVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 商机状态类型 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmBusinessStatusTypeBaseVO {
+
+    @Schema(description = "状态类型名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    @NotNull(message = "状态类型名不能为空")
+    private String name;
+
+    @Schema(description = "使用的部门编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "使用的部门编号不能为空")
+    private String deptIds;
+
+    @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "开启状态不能为空")
+    private Boolean status;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java
new file mode 100644
index 000000000..e9f958a6c
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 商机状态类型创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusTypeCreateReqVO extends CrmBusinessStatusTypeBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java
new file mode 100644
index 000000000..ea82616de
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+
+/**
+ * 商机状态类型 Excel VO
+ *
+ * @author ljlleo
+ */
+@Data
+public class CrmBusinessStatusTypeExcelVO {
+
+    @ExcelProperty("主键")
+    private Long id;
+
+    @ExcelProperty("状态类型名")
+    private String name;
+
+    @ExcelProperty("使用的部门编号")
+    private String deptIds;
+
+    @ExcelProperty("开启状态")
+    private Boolean status;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java
new file mode 100644
index 000000000..48d5bc7ed
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import java.time.LocalDateTime;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 商机状态类型 Excel 导出 Request VO,参数和 CrmBusinessStatusTypePageReqVO 是一致的")
+@Data
+public class CrmBusinessStatusTypeExportReqVO {
+
+    @Schema(description = "状态类型名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "使用的部门编号")
+    private String deptIds;
+
+    @Schema(description = "开启状态", example = "1")
+    private Boolean status;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypePageReqVO.java
new file mode 100644
index 000000000..4b15210ac
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypePageReqVO.java
@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.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;
+
+@Schema(description = "管理后台 - 商机状态类型分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusTypePageReqVO extends PageParam {
+
+    @Schema(description = "状态类型名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "开启状态", example = "1")
+    private Boolean status;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeRespVO.java
new file mode 100644
index 000000000..a4e21c58e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 商机状态类型 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusTypeRespVO extends CrmBusinessStatusTypeBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "24019")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java
new file mode 100644
index 000000000..5fab5fc2f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 商机状态类型更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusTypeUpdateReqVO extends CrmBusinessStatusTypeBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "24019")
+    @NotNull(message = "主键不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/businessstatus/CrmBusinessStatusConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/businessstatus/CrmBusinessStatusConvert.java
new file mode 100644
index 000000000..c8b854144
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/businessstatus/CrmBusinessStatusConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.businessstatus;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
+
+/**
+ * 商机状态 Convert
+ *
+ * @author ljlleo
+ */
+@Mapper
+public interface CrmBusinessStatusConvert {
+
+    CrmBusinessStatusConvert INSTANCE = Mappers.getMapper(CrmBusinessStatusConvert.class);
+
+    CrmBusinessStatusDO convert(CrmBusinessStatusCreateReqVO bean);
+
+    CrmBusinessStatusDO convert(CrmBusinessStatusUpdateReqVO bean);
+
+    CrmBusinessStatusRespVO convert(CrmBusinessStatusDO bean);
+
+    List<CrmBusinessStatusRespVO> convertList(List<CrmBusinessStatusDO> list);
+
+    PageResult<CrmBusinessStatusRespVO> convertPage(PageResult<CrmBusinessStatusDO> page);
+
+    List<CrmBusinessStatusExcelVO> convertList02(List<CrmBusinessStatusDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/businessstatustype/CrmBusinessStatusTypeConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/businessstatustype/CrmBusinessStatusTypeConvert.java
new file mode 100644
index 000000000..75f1aed4c
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/businessstatustype/CrmBusinessStatusTypeConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.businessstatustype;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
+
+/**
+ * 商机状态类型 Convert
+ *
+ * @author ljlleo
+ */
+@Mapper
+public interface CrmBusinessStatusTypeConvert {
+
+    CrmBusinessStatusTypeConvert INSTANCE = Mappers.getMapper(CrmBusinessStatusTypeConvert.class);
+
+    CrmBusinessStatusTypeDO convert(CrmBusinessStatusTypeCreateReqVO bean);
+
+    CrmBusinessStatusTypeDO convert(CrmBusinessStatusTypeUpdateReqVO bean);
+
+    CrmBusinessStatusTypeRespVO convert(CrmBusinessStatusTypeDO bean);
+
+    List<CrmBusinessStatusTypeRespVO> convertList(List<CrmBusinessStatusTypeDO> list);
+
+    PageResult<CrmBusinessStatusTypeRespVO> convertPage(PageResult<CrmBusinessStatusTypeDO> page);
+
+    List<CrmBusinessStatusTypeExcelVO> convertList02(List<CrmBusinessStatusTypeDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
index 795ddad39..2d6454838 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
@@ -1,12 +1,13 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.business;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 /**
@@ -36,13 +37,13 @@ public class CrmBusinessDO extends BaseDO {
     /**
      * 商机状态类型编号
      *
-     *  TODO @ljileo:这个字段,后续要写下关联的实体哈
+     *  关联 {@link CrmBusinessStatusTypeDO#getId()}
      */
     private Long statusTypeId;
     /**
      * 商机状态编号
      *
-     * TODO @ljileo:这个字段,后续要写下关联的实体哈
+     * 关联 {@link CrmBusinessStatusDO#getId()}
      */
     private Long statusId;
     /**
@@ -62,21 +63,18 @@ public class CrmBusinessDO extends BaseDO {
     /**
      * 商机金额
      *
-     * TODO @lijie:Integer
      */
-    private BigDecimal price;
+    private Integer price;
     /**
      * 整单折扣
      *
-     * TODO @lijie:Integer
      */
-    private BigDecimal discountPercent;
+    private Integer discountPercent;
     /**
      * 产品总金额
      *
-     * TODO @lijie:Integer
      */
-    private BigDecimal productPrice;
+    private Integer productPrice;
     /**
      * 备注
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatus/CrmBusinessStatusDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatus/CrmBusinessStatusDO.java
new file mode 100644
index 000000000..a60c6e40d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatus/CrmBusinessStatusDO.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus;
+
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 商机状态 DO
+ *
+ * @author ljlleo
+ */
+@TableName("crm_business_status")
+@KeySequence("crm_business_status_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmBusinessStatusDO {
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 状态类型编号
+     */
+    private Long typeId;
+    /**
+     * 状态名
+     */
+    private String name;
+    /**
+     * 赢单率
+     */
+    private String percent;
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatustype/CrmBusinessStatusTypeDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatustype/CrmBusinessStatusTypeDO.java
new file mode 100644
index 000000000..4a2005e49
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatustype/CrmBusinessStatusTypeDO.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype;
+
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 商机状态类型 DO
+ *
+ * @author ljlleo
+ */
+@TableName("crm_business_status_type")
+@KeySequence("crm_business_status_type_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmBusinessStatusTypeDO {
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 状态类型名
+     */
+    private String name;
+    /**
+     * 使用的部门编号
+     */
+    private String deptIds;
+    /**
+     * 开启状态
+     */
+    private Boolean status;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
index aadec8517..1429c0794 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
@@ -21,23 +21,6 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
     default PageResult<CrmBusinessDO> selectPage(CrmBusinessPageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<CrmBusinessDO>()
                 .likeIfPresent(CrmBusinessDO::getName, reqVO.getName())
-                .eqIfPresent(CrmBusinessDO::getStatusTypeId, reqVO.getStatusTypeId())
-                .eqIfPresent(CrmBusinessDO::getStatusId, reqVO.getStatusId())
-                .betweenIfPresent(CrmBusinessDO::getContactNextTime, reqVO.getContactNextTime())
-                .eqIfPresent(CrmBusinessDO::getCustomerId, reqVO.getCustomerId())
-                .betweenIfPresent(CrmBusinessDO::getDealTime, reqVO.getDealTime())
-                .eqIfPresent(CrmBusinessDO::getPrice, reqVO.getPrice())
-                .eqIfPresent(CrmBusinessDO::getDiscountPercent, reqVO.getDiscountPercent())
-                .eqIfPresent(CrmBusinessDO::getProductPrice, reqVO.getProductPrice())
-                .eqIfPresent(CrmBusinessDO::getRemark, reqVO.getRemark())
-                .eqIfPresent(CrmBusinessDO::getOwnerUserId, reqVO.getOwnerUserId())
-                .betweenIfPresent(CrmBusinessDO::getCreateTime, reqVO.getCreateTime())
-                .eqIfPresent(CrmBusinessDO::getRoUserIds, reqVO.getRoUserIds())
-                .eqIfPresent(CrmBusinessDO::getRwUserIds, reqVO.getRwUserIds())
-                .eqIfPresent(CrmBusinessDO::getEndStatus, reqVO.getEndStatus())
-                .eqIfPresent(CrmBusinessDO::getEndRemark, reqVO.getEndRemark())
-                .betweenIfPresent(CrmBusinessDO::getContactLastTime, reqVO.getContactLastTime())
-                .eqIfPresent(CrmBusinessDO::getFollowUpStatus, reqVO.getFollowUpStatus())
                 .orderByDesc(CrmBusinessDO::getId));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatus/CrmBusinessStatusMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatus/CrmBusinessStatusMapper.java
new file mode 100644
index 000000000..2b9eeb4ae
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatus/CrmBusinessStatusMapper.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.businessstatus;
+
+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.businessstatus.vo.CrmBusinessStatusExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 商机状态 Mapper
+ *
+ * @author ljlleo
+ */
+@Mapper
+public interface CrmBusinessStatusMapper extends BaseMapperX<CrmBusinessStatusDO> {
+
+    default PageResult<CrmBusinessStatusDO> selectPage(CrmBusinessStatusPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmBusinessStatusDO>()
+                .eqIfPresent(CrmBusinessStatusDO::getTypeId, reqVO.getTypeId())
+                .orderByDesc(CrmBusinessStatusDO::getId));
+    }
+
+    default List<CrmBusinessStatusDO> selectList(CrmBusinessStatusExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CrmBusinessStatusDO>()
+                .eqIfPresent(CrmBusinessStatusDO::getTypeId, reqVO.getTypeId())
+                .likeIfPresent(CrmBusinessStatusDO::getName, reqVO.getName())
+                .eqIfPresent(CrmBusinessStatusDO::getPercent, reqVO.getPercent())
+                .eqIfPresent(CrmBusinessStatusDO::getSort, reqVO.getSort())
+                .orderByDesc(CrmBusinessStatusDO::getId));
+    }
+
+    default List<CrmBusinessStatusDO> getBusinessStatusListByTypeId(Integer typeId) {
+        return selectList(CrmBusinessStatusDO::getTypeId, typeId);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatustype/CrmBusinessStatusTypeMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatustype/CrmBusinessStatusTypeMapper.java
new file mode 100644
index 000000000..bc23b2e8b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatustype/CrmBusinessStatusTypeMapper.java
@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.businessstatustype;
+
+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.businessstatustype.vo.CrmBusinessStatusTypeExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypePageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 商机状态类型 Mapper
+ *
+ * @author ljlleo
+ */
+@Mapper
+public interface CrmBusinessStatusTypeMapper extends BaseMapperX<CrmBusinessStatusTypeDO> {
+
+    default PageResult<CrmBusinessStatusTypeDO> selectPage(CrmBusinessStatusTypePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmBusinessStatusTypeDO>()
+                .likeIfPresent(CrmBusinessStatusTypeDO::getName, reqVO.getName())
+                .eqIfPresent(CrmBusinessStatusTypeDO::getDeptIds, reqVO.getDeptIds())
+                .eqIfPresent(CrmBusinessStatusTypeDO::getStatus, reqVO.getStatus())
+                .orderByDesc(CrmBusinessStatusTypeDO::getId));
+    }
+
+    default List<CrmBusinessStatusTypeDO> selectList(CrmBusinessStatusTypeExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CrmBusinessStatusTypeDO>()
+                .likeIfPresent(CrmBusinessStatusTypeDO::getName, reqVO.getName())
+                .eqIfPresent(CrmBusinessStatusTypeDO::getDeptIds, reqVO.getDeptIds())
+                .eqIfPresent(CrmBusinessStatusTypeDO::getStatus, reqVO.getStatus())
+                .orderByDesc(CrmBusinessStatusTypeDO::getId));
+    }
+
+    default List<CrmBusinessStatusTypeDO> getBusinessStatusTypeListByStatus(Integer status) {
+        return selectList(CrmBusinessStatusTypeDO::getStatus, status.byteValue());
+    }
+
+
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusService.java
new file mode 100644
index 000000000..a0dadcd17
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusService.java
@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.crm.service.businessstatus;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 商机状态 Service 接口
+ *
+ * @author ljlleo
+ */
+public interface CrmBusinessStatusService {
+
+    /**
+     * 创建商机状态
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createBusinessStatus(@Valid CrmBusinessStatusCreateReqVO createReqVO);
+
+    /**
+     * 更新商机状态
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateBusinessStatus(@Valid CrmBusinessStatusUpdateReqVO updateReqVO);
+
+    /**
+     * 删除商机状态
+     *
+     * @param id 编号
+     */
+    void deleteBusinessStatus(Long id);
+
+    /**
+     * 获得商机状态
+     *
+     * @param id 编号
+     * @return 商机状态
+     */
+    CrmBusinessStatusDO getBusinessStatus(Long id);
+
+    /**
+     * 获得商机状态列表
+     *
+     * @param ids 编号
+     * @return 商机状态列表
+     */
+    List<CrmBusinessStatusDO> getBusinessStatusList(Collection<Long> ids);
+
+    /**
+     * 获得商机状态分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 商机状态分页
+     */
+    PageResult<CrmBusinessStatusDO> getBusinessStatusPage(CrmBusinessStatusPageReqVO pageReqVO);
+
+    /**
+     * 获得商机状态列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 商机状态列表
+     */
+    List<CrmBusinessStatusDO> getBusinessStatusList(CrmBusinessStatusExportReqVO exportReqVO);
+
+    /**
+     * 根据类型ID获得商机状态列表
+     *
+     * @param typeId 商机状态类型ID
+     * @return 商机状态列表
+     */
+    List<CrmBusinessStatusDO> getBusinessStatusListByTypeId(Integer typeId);
+
+    /**
+     * 获得商机状态列表
+     *
+     * @return 商机状态列表
+     */
+    List<CrmBusinessStatusDO> getBusinessStatusList();
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImpl.java
new file mode 100644
index 000000000..e927fac74
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImpl.java
@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.module.crm.service.businessstatus;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.businessstatus.CrmBusinessStatusConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.businessstatus.CrmBusinessStatusMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_STATUS_NOT_EXISTS;
+
+/**
+ * 商机状态 Service 实现类
+ *
+ * @author ljlleo
+ */
+@Service
+@Validated
+public class CrmBusinessStatusServiceImpl implements CrmBusinessStatusService {
+
+    @Resource
+    private CrmBusinessStatusMapper businessStatusMapper;
+
+    @Override
+    public Long createBusinessStatus(CrmBusinessStatusCreateReqVO createReqVO) {
+        // 插入
+        CrmBusinessStatusDO businessStatus = CrmBusinessStatusConvert.INSTANCE.convert(createReqVO);
+        businessStatusMapper.insert(businessStatus);
+        // 返回
+        return businessStatus.getId();
+    }
+
+    @Override
+    public void updateBusinessStatus(CrmBusinessStatusUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateBusinessStatusExists(updateReqVO.getId());
+        // 更新
+        CrmBusinessStatusDO updateObj = CrmBusinessStatusConvert.INSTANCE.convert(updateReqVO);
+        businessStatusMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteBusinessStatus(Long id) {
+        // 校验存在
+        validateBusinessStatusExists(id);
+        // 删除
+        businessStatusMapper.deleteById(id);
+    }
+
+    private void validateBusinessStatusExists(Long id) {
+        if (businessStatusMapper.selectById(id) == null) {
+            throw exception(BUSINESS_STATUS_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmBusinessStatusDO getBusinessStatus(Long id) {
+        return businessStatusMapper.selectById(id);
+    }
+
+    @Override
+    public List<CrmBusinessStatusDO> getBusinessStatusList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return businessStatusMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CrmBusinessStatusDO> getBusinessStatusPage(CrmBusinessStatusPageReqVO pageReqVO) {
+        return businessStatusMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CrmBusinessStatusDO> getBusinessStatusList(CrmBusinessStatusExportReqVO exportReqVO) {
+        return businessStatusMapper.selectList(exportReqVO);
+    }
+
+    @Override
+    public List<CrmBusinessStatusDO> getBusinessStatusListByTypeId(Integer typeId) {
+        return businessStatusMapper.getBusinessStatusListByTypeId(typeId);
+    }
+
+    @Override
+    public List<CrmBusinessStatusDO> getBusinessStatusList() {
+        return businessStatusMapper.selectList();
+    }
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeService.java
new file mode 100644
index 000000000..3c473f62b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeService.java
@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.crm.service.businessstatustype;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 商机状态类型 Service 接口
+ *
+ * @author ljlleo
+ */
+public interface CrmBusinessStatusTypeService {
+
+    /**
+     * 创建商机状态类型
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createBusinessStatusType(@Valid CrmBusinessStatusTypeCreateReqVO createReqVO);
+
+    /**
+     * 更新商机状态类型
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateBusinessStatusType(@Valid CrmBusinessStatusTypeUpdateReqVO updateReqVO);
+
+    /**
+     * 删除商机状态类型
+     *
+     * @param id 编号
+     */
+    void deleteBusinessStatusType(Long id);
+
+    /**
+     * 获得商机状态类型
+     *
+     * @param id 编号
+     * @return 商机状态类型
+     */
+    CrmBusinessStatusTypeDO getBusinessStatusType(Long id);
+
+    /**
+     * 获得商机状态类型列表
+     *
+     * @param ids 编号
+     * @return 商机状态类型列表
+     */
+    List<CrmBusinessStatusTypeDO> getBusinessStatusTypeList(Collection<Long> ids);
+
+    /**
+     * 获得商机状态类型分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 商机状态类型分页
+     */
+    PageResult<CrmBusinessStatusTypeDO> getBusinessStatusTypePage(CrmBusinessStatusTypePageReqVO pageReqVO);
+
+    /**
+     * 获得商机状态类型列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 商机状态类型列表
+     */
+    List<CrmBusinessStatusTypeDO> getBusinessStatusTypeList(CrmBusinessStatusTypeExportReqVO exportReqVO);
+
+    /**
+     * 获得商机状态类型列表
+     *
+     * @param status 状态
+     * @return 商机状态类型列表
+     */
+    List<CrmBusinessStatusTypeDO> getBusinessStatusTypeListByStatus(Integer status);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImpl.java
new file mode 100644
index 000000000..88b25ad1e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImpl.java
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.crm.service.businessstatustype;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.businessstatustype.CrmBusinessStatusTypeConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.businessstatustype.CrmBusinessStatusTypeMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_STATUS_TYPE_NOT_EXISTS;
+
+/**
+ * 商机状态类型 Service 实现类
+ *
+ * @author ljlleo
+ */
+@Service
+@Validated
+public class CrmBusinessStatusTypeServiceImpl implements CrmBusinessStatusTypeService {
+
+    @Resource
+    private CrmBusinessStatusTypeMapper businessStatusTypeMapper;
+
+    @Override
+    public Long createBusinessStatusType(CrmBusinessStatusTypeCreateReqVO createReqVO) {
+        // 插入
+        CrmBusinessStatusTypeDO businessStatusType = CrmBusinessStatusTypeConvert.INSTANCE.convert(createReqVO);
+        businessStatusTypeMapper.insert(businessStatusType);
+        // 返回
+        return businessStatusType.getId();
+    }
+
+    @Override
+    public void updateBusinessStatusType(CrmBusinessStatusTypeUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateBusinessStatusTypeExists(updateReqVO.getId());
+        // 更新
+        CrmBusinessStatusTypeDO updateObj = CrmBusinessStatusTypeConvert.INSTANCE.convert(updateReqVO);
+        businessStatusTypeMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteBusinessStatusType(Long id) {
+        // 校验存在
+        validateBusinessStatusTypeExists(id);
+        // 删除
+        businessStatusTypeMapper.deleteById(id);
+    }
+
+    private void validateBusinessStatusTypeExists(Long id) {
+        if (businessStatusTypeMapper.selectById(id) == null) {
+            throw exception(BUSINESS_STATUS_TYPE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmBusinessStatusTypeDO getBusinessStatusType(Long id) {
+        return businessStatusTypeMapper.selectById(id);
+    }
+
+    @Override
+    public List<CrmBusinessStatusTypeDO> getBusinessStatusTypeList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return businessStatusTypeMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CrmBusinessStatusTypeDO> getBusinessStatusTypePage(CrmBusinessStatusTypePageReqVO pageReqVO) {
+        return businessStatusTypeMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CrmBusinessStatusTypeDO> getBusinessStatusTypeList(CrmBusinessStatusTypeExportReqVO exportReqVO) {
+        return businessStatusTypeMapper.selectList(exportReqVO);
+    }
+
+    @Override
+    public List<CrmBusinessStatusTypeDO> getBusinessStatusTypeListByStatus(Integer status) {
+        return businessStatusTypeMapper.getBusinessStatusTypeListByStatus(status);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/businessstatustype/CrmBusinessStatusTypeMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/businessstatustype/CrmBusinessStatusTypeMapper.xml
new file mode 100644
index 000000000..44fbd07ec
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/businessstatustype/CrmBusinessStatusTypeMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.businessstatustype.CrmBusinessStatusTypeMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImplTest.java
new file mode 100644
index 000000000..44030db3b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImplTest.java
@@ -0,0 +1,170 @@
+package cn.iocoder.yudao.module.crm.service.businessstatus;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.businessstatus.CrmBusinessStatusMapper;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_STATUS_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link CrmBusinessStatusServiceImpl} 的单元测试类
+ *
+ * @author ljlleo
+ */
+@Import(CrmBusinessStatusServiceImpl.class)
+public class CrmBusinessStatusServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmBusinessStatusServiceImpl businessStatusService;
+
+    @Resource
+    private CrmBusinessStatusMapper businessStatusMapper;
+
+    @Test
+    public void testCreateBusinessStatus_success() {
+        // 准备参数
+        CrmBusinessStatusCreateReqVO reqVO = randomPojo(CrmBusinessStatusCreateReqVO.class);
+
+        // 调用
+        Long businessStatusId = businessStatusService.createBusinessStatus(reqVO);
+        // 断言
+        assertNotNull(businessStatusId);
+        // 校验记录的属性是否正确
+        CrmBusinessStatusDO businessStatus = businessStatusMapper.selectById(businessStatusId);
+        assertPojoEquals(reqVO, businessStatus);
+    }
+
+    @Test
+    public void testUpdateBusinessStatus_success() {
+        // mock 数据
+        CrmBusinessStatusDO dbBusinessStatus = randomPojo(CrmBusinessStatusDO.class);
+        businessStatusMapper.insert(dbBusinessStatus);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmBusinessStatusUpdateReqVO reqVO = randomPojo(CrmBusinessStatusUpdateReqVO.class, o -> {
+            o.setId(dbBusinessStatus.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        businessStatusService.updateBusinessStatus(reqVO);
+        // 校验是否更新正确
+        CrmBusinessStatusDO businessStatus = businessStatusMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, businessStatus);
+    }
+
+    @Test
+    public void testUpdateBusinessStatus_notExists() {
+        // 准备参数
+        CrmBusinessStatusUpdateReqVO reqVO = randomPojo(CrmBusinessStatusUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> businessStatusService.updateBusinessStatus(reqVO), BUSINESS_STATUS_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteBusinessStatus_success() {
+        // mock 数据
+        CrmBusinessStatusDO dbBusinessStatus = randomPojo(CrmBusinessStatusDO.class);
+        businessStatusMapper.insert(dbBusinessStatus);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbBusinessStatus.getId();
+
+        // 调用
+        businessStatusService.deleteBusinessStatus(id);
+       // 校验数据不存在了
+       assertNull(businessStatusMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteBusinessStatus_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> businessStatusService.deleteBusinessStatus(id), BUSINESS_STATUS_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetBusinessStatusPage() {
+       // mock 数据
+       CrmBusinessStatusDO dbBusinessStatus = randomPojo(CrmBusinessStatusDO.class, o -> { // 等会查询到
+           o.setTypeId(null);
+           o.setName(null);
+           o.setPercent(null);
+           o.setSort(null);
+       });
+       businessStatusMapper.insert(dbBusinessStatus);
+       // 测试 typeId 不匹配
+       businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setTypeId(null)));
+       // 测试 name 不匹配
+       businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setName(null)));
+       // 测试 percent 不匹配
+       businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setPercent(null)));
+       // 测试 sort 不匹配
+       businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setSort(null)));
+       // 准备参数
+       CrmBusinessStatusPageReqVO reqVO = new CrmBusinessStatusPageReqVO();
+       reqVO.setTypeId(null);
+       reqVO.setName(null);
+       reqVO.setPercent(null);
+       reqVO.setSort(null);
+
+       // 调用
+       PageResult<CrmBusinessStatusDO> pageResult = businessStatusService.getBusinessStatusPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbBusinessStatus, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetBusinessStatusList() {
+       // mock 数据
+       CrmBusinessStatusDO dbBusinessStatus = randomPojo(CrmBusinessStatusDO.class, o -> { // 等会查询到
+           o.setTypeId(null);
+           o.setName(null);
+           o.setPercent(null);
+           o.setSort(null);
+       });
+       businessStatusMapper.insert(dbBusinessStatus);
+       // 测试 typeId 不匹配
+       businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setTypeId(null)));
+       // 测试 name 不匹配
+       businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setName(null)));
+       // 测试 percent 不匹配
+       businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setPercent(null)));
+       // 测试 sort 不匹配
+       businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setSort(null)));
+       // 准备参数
+       CrmBusinessStatusExportReqVO reqVO = new CrmBusinessStatusExportReqVO();
+       reqVO.setTypeId(null);
+       reqVO.setName(null);
+       reqVO.setPercent(null);
+       reqVO.setSort(null);
+
+       // 调用
+       List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusList(reqVO);
+       // 断言
+       assertEquals(1, list.size());
+       assertPojoEquals(dbBusinessStatus, list.get(0));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImplTest.java
new file mode 100644
index 000000000..58f054b1c
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImplTest.java
@@ -0,0 +1,171 @@
+package cn.iocoder.yudao.module.crm.service.businessstatustype;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.CrmBusinessStatusTypeUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.businessstatustype.CrmBusinessStatusTypeMapper;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_STATUS_TYPE_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link CrmBusinessStatusTypeServiceImpl} 的单元测试类
+ *
+ * @author ljlleo
+ */
+@Import(CrmBusinessStatusTypeServiceImpl.class)
+public class CrmBusinessStatusTypeServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmBusinessStatusTypeServiceImpl businessStatusTypeService;
+
+    @Resource
+    private CrmBusinessStatusTypeMapper businessStatusTypeMapper;
+
+    @Test
+    public void testCreateBusinessStatusType_success() {
+        // 准备参数
+        CrmBusinessStatusTypeCreateReqVO reqVO = randomPojo(CrmBusinessStatusTypeCreateReqVO.class);
+
+        // 调用
+        Long businessStatusTypeId = businessStatusTypeService.createBusinessStatusType(reqVO);
+        // 断言
+        assertNotNull(businessStatusTypeId);
+        // 校验记录的属性是否正确
+        CrmBusinessStatusTypeDO businessStatusType = businessStatusTypeMapper.selectById(businessStatusTypeId);
+        assertPojoEquals(reqVO, businessStatusType);
+    }
+
+    @Test
+    public void testUpdateBusinessStatusType_success() {
+        // mock 数据
+        CrmBusinessStatusTypeDO dbBusinessStatusType = randomPojo(CrmBusinessStatusTypeDO.class);
+        businessStatusTypeMapper.insert(dbBusinessStatusType);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmBusinessStatusTypeUpdateReqVO reqVO = randomPojo(CrmBusinessStatusTypeUpdateReqVO.class, o -> {
+            o.setId(dbBusinessStatusType.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        businessStatusTypeService.updateBusinessStatusType(reqVO);
+        // 校验是否更新正确
+        CrmBusinessStatusTypeDO businessStatusType = businessStatusTypeMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, businessStatusType);
+    }
+
+    @Test
+    public void testUpdateBusinessStatusType_notExists() {
+        // 准备参数
+        CrmBusinessStatusTypeUpdateReqVO reqVO = randomPojo(CrmBusinessStatusTypeUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> businessStatusTypeService.updateBusinessStatusType(reqVO), BUSINESS_STATUS_TYPE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteBusinessStatusType_success() {
+        // mock 数据
+        CrmBusinessStatusTypeDO dbBusinessStatusType = randomPojo(CrmBusinessStatusTypeDO.class);
+        businessStatusTypeMapper.insert(dbBusinessStatusType);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbBusinessStatusType.getId();
+
+        // 调用
+        businessStatusTypeService.deleteBusinessStatusType(id);
+       // 校验数据不存在了
+       assertNull(businessStatusTypeMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteBusinessStatusType_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> businessStatusTypeService.deleteBusinessStatusType(id), BUSINESS_STATUS_TYPE_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetBusinessStatusTypePage() {
+       // mock 数据
+       CrmBusinessStatusTypeDO dbBusinessStatusType = randomPojo(CrmBusinessStatusTypeDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setDeptIds(null);
+           o.setStatus(null);
+           o.setCreateTime(null);
+       });
+       businessStatusTypeMapper.insert(dbBusinessStatusType);
+       // 测试 name 不匹配
+       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setName(null)));
+       // 测试 deptIds 不匹配
+       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setDeptIds(null)));
+       // 测试 status 不匹配
+       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setStatus(null)));
+       // 测试 createTime 不匹配
+       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmBusinessStatusTypePageReqVO reqVO = new CrmBusinessStatusTypePageReqVO();
+       reqVO.setName(null);
+       reqVO.setDeptIds(null);
+       reqVO.setStatus(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<CrmBusinessStatusTypeDO> pageResult = businessStatusTypeService.getBusinessStatusTypePage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbBusinessStatusType, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetBusinessStatusTypeList() {
+       // mock 数据
+       CrmBusinessStatusTypeDO dbBusinessStatusType = randomPojo(CrmBusinessStatusTypeDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setDeptIds(null);
+           o.setStatus(null);
+           o.setCreateTime(null);
+       });
+       businessStatusTypeMapper.insert(dbBusinessStatusType);
+       // 测试 name 不匹配
+       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setName(null)));
+       // 测试 deptIds 不匹配
+       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setDeptIds(null)));
+       // 测试 status 不匹配
+       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setStatus(null)));
+       // 测试 createTime 不匹配
+       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmBusinessStatusTypeExportReqVO reqVO = new CrmBusinessStatusTypeExportReqVO();
+       reqVO.setName(null);
+       reqVO.setDeptIds(null);
+       reqVO.setStatus(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeList(reqVO);
+       // 断言
+       assertEquals(1, list.size());
+       assertPojoEquals(dbBusinessStatusType, list.get(0));
+    }
+
+}
diff --git a/yudao-ui-admin/src/api/crm/business.js b/yudao-ui-admin/src/api/crm/business.js
new file mode 100644
index 000000000..24789df72
--- /dev/null
+++ b/yudao-ui-admin/src/api/crm/business.js
@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 创建商机
+export function createBusiness(data) {
+  return request({
+    url: '/crm/business/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新商机
+export function updateBusiness(data) {
+  return request({
+    url: '/crm/business/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除商机
+export function deleteBusiness(id) {
+  return request({
+    url: '/crm/business/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得商机
+export function getBusiness(id) {
+  return request({
+    url: '/crm/business/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得商机分页
+export function getBusinessPage(query) {
+  return request({
+    url: '/crm/business/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出商机 Excel
+export function exportBusinessExcel(query) {
+  return request({
+    url: '/crm/business/export-excel',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}
diff --git a/yudao-ui-admin/src/api/crm/businessStatus.js b/yudao-ui-admin/src/api/crm/businessStatus.js
new file mode 100644
index 000000000..d4249d7ac
--- /dev/null
+++ b/yudao-ui-admin/src/api/crm/businessStatus.js
@@ -0,0 +1,70 @@
+import request from '@/utils/request'
+
+// 创建商机状态
+export function createBusinessStatus(data) {
+  return request({
+    url: '/crm/business-status/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新商机状态
+export function updateBusinessStatus(data) {
+  return request({
+    url: '/crm/business-status/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除商机状态
+export function deleteBusinessStatus(id) {
+  return request({
+    url: '/crm/business-status/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得商机状态
+export function getBusinessStatus(id) {
+  return request({
+    url: '/crm/business-status/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得商机状态分页
+export function getBusinessStatusPage(query) {
+  return request({
+    url: '/crm/business-status/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出商机状态 Excel
+export function exportBusinessStatusExcel(query) {
+  return request({
+    url: '/crm/business-status/export-excel',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}
+
+// 根据类型ID获取商机状态信息列表
+export function getBusinessStatusListByTypeId(typeId) {
+  return request({
+    url: '/crm/business-status/get-simple-list?typeId=' + typeId,
+    method: 'get'
+  })
+}
+
+// 获取商机状态信息列表
+export function getBusinessStatusList() {
+  return request({
+    url: '/crm/business-status/get-all-list',
+    method: 'get'
+  })
+}
diff --git a/yudao-ui-admin/src/api/crm/businessStatusType.js b/yudao-ui-admin/src/api/crm/businessStatusType.js
new file mode 100644
index 000000000..5df0aff6c
--- /dev/null
+++ b/yudao-ui-admin/src/api/crm/businessStatusType.js
@@ -0,0 +1,62 @@
+import request from '@/utils/request'
+
+// 创建商机状态类型
+export function createBusinessStatusType(data) {
+  return request({
+    url: '/crm/business-status-type/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新商机状态类型
+export function updateBusinessStatusType(data) {
+  return request({
+    url: '/crm/business-status-type/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除商机状态类型
+export function deleteBusinessStatusType(id) {
+  return request({
+    url: '/crm/business-status-type/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得商机状态类型
+export function getBusinessStatusType(id) {
+  return request({
+    url: '/crm/business-status-type/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得商机状态类型分页
+export function getBusinessStatusTypePage(query) {
+  return request({
+    url: '/crm/business-status-type/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出商机状态类型 Excel
+export function exportBusinessStatusTypeExcel(query) {
+  return request({
+    url: '/crm/business-status-type/export-excel',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}
+
+// 获取商机状态类型信息列表
+export function getBusinessStatusTypeList() {
+  return request({
+    url: '/crm/business-status-type/get-simple-list',
+    method: 'get'
+  })
+}
diff --git a/yudao-ui-admin/src/views/crm/business/index.vue b/yudao-ui-admin/src/views/crm/business/index.vue
new file mode 100644
index 000000000..b877a33af
--- /dev/null
+++ b/yudao-ui-admin/src/views/crm/business/index.vue
@@ -0,0 +1,335 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="商机名称" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入商机名称" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['crm:business:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['crm:business:export']">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="商机名称" align="center" prop="name" />
+      <el-table-column label="客户名称" align="center" prop="customerId" />
+      <el-table-column label="商机金额" align="center" prop="price" />
+      <el-table-column label="预计成交日期" align="center" prop="dealTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.dealTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="商机状态类型" align="center" prop="statusTypeId" width="120">
+        <template v-slot="scope">
+          <el-tag> {{getBusinessStatusTypeName(scope.row.statusTypeId)}} </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="商机状态" align="center" prop="statusId" width="100">
+        <template v-slot="scope">
+          <el-tag> {{getBusinessStatusName(scope.row.statusId)}} </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="更新时间" align="center" prop="updateTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.updateTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="负责人" align="center" prop="ownerUserId" />
+      <el-table-column label="创建人" align="center" prop="creator" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['crm:business:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['crm:business:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" v-dialogDrag append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="商机名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入商机名称" />
+        </el-form-item>
+        <el-form-item label="客户编号" prop="customerId">
+          <el-input v-model="form.customerId" placeholder="请输入客户编号" />
+        </el-form-item>
+        <el-form-item label="商机状态类型" prop="statusTypeId">
+          <el-select v-model="form.statusTypeId" placeholder="请选择商机状态类型" clearable size="small" @change="changeBusinessStatusType">
+            <el-option v-for="item in businessStatusTypeList" :key="item.id" :label="item.name" :value="item.id"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="商机状态" prop="statusId">
+          <el-select v-model="form.statusId" placeholder="请选择商机状态" clearable size="small">
+            <el-option v-for="item in businessStatusList" :key="item.id" :label="item.name" :value="item.id"/>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预计成交日期" prop="dealTime">
+          <el-date-picker clearable v-model="form.dealTime" type="date" value-format="timestamp" placeholder="选择预计成交日期" />
+        </el-form-item>
+        <el-form-item label="商机金额" prop="price">
+          <el-input v-model="form.price" placeholder="请输入商机金额" />
+        </el-form-item>
+        <el-form-item label="整单折扣(%)" prop="discountPercent">
+          <el-input v-model="form.discountPercent" placeholder="请输入整单折扣" />
+        </el-form-item>
+        <el-form-item label="产品总金额" prop="productPrice">
+          <el-input v-model="form.productPrice" placeholder="请输入产品总金额" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { createBusiness, updateBusiness, deleteBusiness, getBusiness, getBusinessPage, exportBusinessExcel } from "@/api/crm/business";
+import { getBusinessStatusListByTypeId, getBusinessStatusList } from "@/api/crm/businessStatus";
+import { getBusinessStatusTypeList } from "@/api/crm/businessStatusType";
+
+export default {
+  name: "Business",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 商机列表
+      list: [],
+      // 根据类型ID获取的商机状态列表
+      businessStatusList: [],
+      // 所有商机状态列表
+      businessStatusAllList: [],
+      // 商机状态类型列表
+      businessStatusTypeList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        name: null,
+        statusTypeId: null,
+        statusId: null,
+        contactNextTime: [],
+        customerId: null,
+        dealTime: [],
+        price: null,
+        discountPercent: null,
+        productPrice: null,
+        remark: null,
+        ownerUserId: null,
+        createTime: [],
+        roUserIds: null,
+        rwUserIds: null,
+        endStatus: null,
+        endRemark: null,
+        contactLastTime: [],
+        followUpStatus: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        name: [{ required: true, message: "商机名称不能为空", trigger: "blur" }],
+        customerId: [{ required: true, message: "客户编号不能为空", trigger: "blur" }],
+        roUserIds: [{ required: true, message: "只读权限的用户编号数组不能为空", trigger: "blur" }],
+        rwUserIds: [{ required: true, message: "读写权限的用户编号数组不能为空", trigger: "blur" }],
+        endStatus: [{ required: true, message: "1赢单2输单3无效不能为空", trigger: "blur" }],
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 执行查询
+      getBusinessPage(this.queryParams).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+      //查询商机状态类型集合
+      getBusinessStatusTypeList().then(response => {
+        this.businessStatusTypeList = response.data;
+      });
+      //查询商机状态类型集合
+      getBusinessStatusList().then(response => {
+        this.businessStatusAllList = response.data;
+      });
+
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        name: undefined,
+        statusTypeId: undefined,
+        statusId: undefined,
+        contactNextTime: undefined,
+        customerId: undefined,
+        dealTime: undefined,
+        price: undefined,
+        discountPercent: undefined,
+        productPrice: undefined,
+        remark: undefined,
+        ownerUserId: undefined,
+        roUserIds: undefined,
+        rwUserIds: undefined,
+        endStatus: undefined,
+        endRemark: undefined,
+        contactLastTime: undefined,
+        followUpStatus: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加商机";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id;
+      getBusiness(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改商机";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 修改的提交
+        if (this.form.id != null) {
+          updateBusiness(this.form).then(response => {
+            this.$modal.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          });
+          return;
+        }
+        // 添加的提交
+        createBusiness(this.form).then(response => {
+          this.$modal.msgSuccess("新增成功");
+          this.open = false;
+          this.getList();
+        });
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$modal.confirm('是否确认删除商机编号为"' + id + '"的数据项?').then(function() {
+          return deleteBusiness(id);
+        }).then(() => {
+          this.getList();
+          this.$modal.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      // 处理查询参数
+      let params = {...this.queryParams};
+      params.pageNo = undefined;
+      params.pageSize = undefined;
+      this.$modal.confirm('是否确认导出所有商机数据项?').then(() => {
+          this.exportLoading = true;
+          return exportBusinessExcel(params);
+        }).then(response => {
+          this.$download.excel(response, '商机.xls');
+          this.exportLoading = false;
+        }).catch(() => {});
+    },
+    /** 选择商机状态类型事件 */
+    changeBusinessStatusType (id) {
+      //查询商机状态集合
+      getBusinessStatusListByTypeId(id).then(response => {
+        this.businessStatusList = response.data;
+      });
+    },
+    /** 商机状态类型名格式化 */
+    getBusinessStatusTypeName(typeId) {
+      for (const item of this.businessStatusTypeList) {
+        if (item.id === typeId) {
+          return item.name;
+        }
+      }
+      return '未知';
+    },
+    /** 商机状态名格式化 */
+    getBusinessStatusName(id) {
+      for (const item of this.businessStatusAllList) {
+        if (item.id === id) {
+          return item.name;
+        }
+      }
+      return '未知';
+    }
+  }
+};
+</script>

From 74f786f8ed2a31e6bd947c42b3eadd92f34df43e Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 27 Oct 2023 22:56:46 +0800
Subject: [PATCH 025/101] =?UTF-8?q?code=20review=EF=BC=9A=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E8=A1=A8=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/customer/vo/CrmCustomerBaseVO.java           |  7 ++++++-
 .../admin/customer/vo/CrmCustomerCreateReqVO.java      |  2 ++
 .../admin/customer/vo/CrmCustomerExcelVO.java          |  2 +-
 .../admin/customer/vo/CrmCustomerExportReqVO.java      |  1 +
 .../admin/customer/vo/CrmCustomerPageReqVO.java        |  3 +++
 .../admin/customer/vo/CrmCustomerUpdateReqVO.java      |  1 +
 .../crm/convert/customer/CrmCustomerConvert.java       |  2 --
 .../module/crm/dal/dataobject/clue/CrmClueDO.java      |  3 ++-
 .../crm/dal/dataobject/customer/CrmCustomerDO.java     | 10 +++-------
 .../module/crm/service/clue/CrmClueServiceImpl.java    |  1 +
 .../service/customer/CrmCustomerServiceImplTest.java   |  1 +
 11 files changed, 21 insertions(+), 12 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
index 039cda53a..0db54fa1f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
@@ -19,14 +19,16 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class CrmCustomerBaseVO {
 
-    @Schema(description = "客户名称", example = "赵六")
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
     @NotEmpty(message = "客户名称不能为空")
     private String name;
 
+    // TODO wanwan:这个字段应该只有 RespVO 会有;创建和修改不传递;
     @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     @NotNull(message = "跟进状态不能为空")
     private Boolean followUpStatus;
 
+    // TODO wanwan:这个字段应该只有 RespVO 会有;创建和修改不传递;
     @Schema(description = "锁定状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     @NotNull(message = "锁定状态不能为空")
     private Boolean lockStatus;
@@ -45,6 +47,7 @@ public class CrmCustomerBaseVO {
     @Schema(description = "备注", example = "随便")
     private String remark;
 
+    // TODO wanwan:这个字段应该只有 RespVO 会有;创建和修改不传递;因为它会在“移交”里面做哈
     @Schema(description = "负责人的用户编号", example = "25682")
     @NotNull(message = "负责人不能为空")
     private Long ownerUserId;
@@ -55,12 +58,14 @@ public class CrmCustomerBaseVO {
     @Schema(description = "详细地址", example = "北京市海淀区")
     private String detailAddress;
 
+    // TODO @芋艿:longitude、latitude 这两个字段删除;
     @Schema(description = "地理位置经度", example = "116.40341")
     private String longitude;
 
     @Schema(description = "地理位置维度", example = "39.92409")
     private String latitude;
 
+    // TODO wanwan:这个字段应该只有 RespVO 会有;创建和修改不传递;
     @Schema(description = "最后跟进时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactLastTime;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
index dae9cf212..a3ed7ef13 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
@@ -11,9 +11,11 @@ import lombok.ToString;
 @ToString(callSuper = true)
 public class CrmCustomerCreateReqVO extends CrmCustomerBaseVO {
 
+    // TODO @wanwan:类型应该是传递 List<Long>; 不过这个字段,默认新建的时候不传递,在“移交”功能里管理
     @Schema(description = "只读权限的用户编号数组")
     private String roUserIds;
 
+    // TODO @wanwan:类型应该是传递 List<Long>; 不过这个字段,默认新建的时候不传递,在“移交”功能里管理
     @Schema(description = "读写权限的用户编号数组")
     private String rwUserIds;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
index 6cf7cbd0f..3e37c30e7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
@@ -8,7 +8,7 @@ import lombok.Data;
 
 import java.time.LocalDateTime;
 
-
+// TODO 芋艿:导出最后做,等基本确认的差不多之后;
 /**
  * 客户 Excel VO
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
index 27198ff0d..15776aa42 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+// TODO 芋艿:导出最后做,等基本确认的差不多之后;
 @Schema(description = "管理后台 - 客户 Excel 导出 Request VO,参数和 CrmCustomerPageReqVO 是一致的")
 @Data
 public class CrmCustomerExportReqVO {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
index 3871e422d..7cb89adbc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
@@ -18,10 +18,13 @@ public class CrmCustomerPageReqVO extends PageParam {
     @Schema(description = "手机", example = "18000000000")
     private String mobile;
 
+    // TODO @wanwan:这个字段不需要哈
     @Schema(description = "电话", example = "18000000000")
     private String telephone;
 
+    // TODO @wanwan:这个字段不需要哈
     @Schema(description = "网址", example = "https://www.baidu.com")
     private String website;
 
+    // TODO @芋艿:场景;
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
index a54938ea2..59d440a35 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
@@ -17,6 +17,7 @@ public class CrmCustomerUpdateReqVO extends CrmCustomerBaseVO {
     @NotNull(message = "编号不能为空")
     private Long id;
 
+    // TODO @wanwan:下面两个字段,同 CrmCustomerCreateReqVO
     @Schema(description = "只读权限的用户编号数组")
     private String roUserIds;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index da683fbfb..937705d99 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -25,8 +25,6 @@ public interface CrmCustomerConvert {
 
     CrmCustomerRespVO convert(CrmCustomerDO bean);
 
-    List<CrmCustomerRespVO> convertList(List<CrmCustomerDO> list);
-
     PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> page);
 
     List<CrmCustomerExcelVO> convertList02(List<CrmCustomerDO> list);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
index 067bec65c..1c6ccd608 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
@@ -44,7 +44,8 @@ public class CrmClueDO extends BaseDO {
     private String name;
     /**
      * 客户 id
-     * 对应 {@link CrmCustomerDO#getId()}
+     *
+     * 关联 {@link CrmCustomerDO#getId()}
      */
     private Long customerId;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index d35650905..101d0dd27 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -4,11 +4,11 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
-import com.sun.xml.bind.v2.TODO;
 import lombok.*;
 
 import java.time.LocalDateTime;
 
+// TODO 芋艿:调整下字段
 /**
  * 客户 DO
  *
@@ -35,20 +35,14 @@ public class CrmCustomerDO extends BaseDO {
     private String name;
     /**
      * 跟进状态
-     * <p>
-     * 枚举 {@link TODO infra_boolean_string 对应的类}
      */
     private Boolean followUpStatus;
     /**
      * 锁定状态
-     * <p>
-     * 枚举 {@link TODO infra_boolean_string 对应的类}
      */
     private Boolean lockStatus;
     /**
      * 成交状态
-     * <p>
-     * 枚举 {@link TODO infra_boolean_string 对应的类}
      */
     private Boolean dealStatus;
     /**
@@ -71,6 +65,7 @@ public class CrmCustomerDO extends BaseDO {
      * 负责人的用户编号
      */
     private Long ownerUserId;
+    // TODO @wanwan:下面两个字段,使用 List<Long>,然后使用 typeHandler = LongListTypeHandler 解决持久化的问题;注意类上需要加 autoResultMap = true
     /**
      * 只读权限的用户编号数组
      */
@@ -87,6 +82,7 @@ public class CrmCustomerDO extends BaseDO {
      * 详细地址
      */
     private String detailAddress;
+    // TODO @wanwan:下面两个字段:删除
     /**
      * 地理位置经度
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
index 3389c4b26..32577dd24 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
@@ -96,6 +96,7 @@ public class CrmClueServiceImpl implements CrmClueService {
         return clueMapper.selectList(exportReqVO);
     }
 
+    // TODO @wanwan:可以在 CrmClueServiceImpl 中,增加一个方法,用于校验客户是否存在;validateCustomer;然后其它方法可以调用它。不过要注意,需要把 CustomerDO 返回,因为其它模块可能要它的信息
     /**
      * 校验客户是否存在
      *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
index 2e7d17907..adce8b163 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
@@ -23,6 +23,7 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
 import static org.junit.jupiter.api.Assertions.*;
 
+// TODO 芋艿:单测后续补
 /**
  * {@link CrmCustomerServiceImpl} 的单元测试类
  *

From 81b6aebf476426e356fd0b68dda5f1b7fd7f3e66 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 27 Oct 2023 23:27:08 +0800
Subject: [PATCH 026/101] =?UTF-8?q?code=20review=EF=BC=9A=E5=95=86?=
 =?UTF-8?q?=E6=9C=BA=E8=A1=A8=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../yudao/module/crm/enums/ErrorCodeConstants.java   |  3 +++
 .../businessstatus/CrmBusinessStatusController.java  |  4 ++++
 .../businessstatus/vo/CrmBusinessStatusBaseVO.java   |  4 ++++
 .../businessstatus/vo/CrmBusinessStatusExcelVO.java  |  6 ++----
 .../vo/CrmBusinessStatusExportReqVO.java             |  5 ++---
 .../businessstatus/vo/CrmBusinessStatusRespVO.java   |  2 +-
 .../vo/CrmBusinessStatusUpdateReqVO.java             | 12 +++++++-----
 .../CrmBusinessStatusTypeController.java             |  2 ++
 .../vo/CrmBusinessStatusTypeCreateReqVO.java         |  1 +
 .../vo/CrmBusinessStatusTypeExcelVO.java             | 10 ++++------
 .../vo/CrmBusinessStatusTypeExportReqVO.java         |  8 ++++----
 .../vo/CrmBusinessStatusTypeUpdateReqVO.java         |  9 ++++++---
 .../businessstatus/CrmBusinessStatusDO.java          |  2 ++
 .../businessstatustype/CrmBusinessStatusTypeDO.java  |  2 ++
 .../CrmBusinessStatusTypeMapper.java                 |  4 +---
 .../businessstatus/CrmBusinessStatusService.java     |  4 ++--
 .../CrmBusinessStatusTypeServiceImpl.java            |  3 +++
 17 files changed, 50 insertions(+), 31 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 374687e5e..124a70cb7 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -17,6 +17,9 @@ public interface ErrorCodeConstants {
 
     // ========== 商机管理 1-020-002-000 ==========
     ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
+
+    // TODO @lilleo:商机状态、商机类型,都单独错误码段
+
     ErrorCode BUSINESS_STATUS_TYPE_NOT_EXISTS = new ErrorCode(1_020_002_001, "商机状态类型不存在");
     ErrorCode BUSINESS_STATUS_NOT_EXISTS = new ErrorCode(1_020_002_002, "商机状态不存在");
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java
index fddec8d8d..275285098 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java
@@ -25,6 +25,7 @@ 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;
 
+// TODO @lilleo:这个模块,可以挪到 business 下;这样我打开 business 包下,就知道,噢~原来里面有 business 商机、有 type 状态类型、status 具体状态;
 @Tag(name = "管理后台 - 商机状态")
 @RestController
 @RequestMapping("/crm/business-status")
@@ -67,6 +68,7 @@ public class CrmBusinessStatusController {
         return success(CrmBusinessStatusConvert.INSTANCE.convert(businessStatus));
     }
 
+    // TODO @lilleo:这个接口,暂时用不到,可以考虑先删除掉
     @GetMapping("/list")
     @Operation(summary = "获得商机状态列表")
     @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
@@ -96,6 +98,7 @@ public class CrmBusinessStatusController {
         ExcelUtils.write(response, "商机状态.xls", "数据", CrmBusinessStatusExcelVO.class, datas);
     }
 
+    // TODO 芋艿:后续再看看
     @GetMapping("/get-simple-list")
     @Operation(summary = "获得商机状态列表")
     @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
@@ -104,6 +107,7 @@ public class CrmBusinessStatusController {
         return success(CrmBusinessStatusConvert.INSTANCE.convertList(list));
     }
 
+    // TODO 芋艿:后续再看看
     @GetMapping("/get-all-list")
     @Operation(summary = "获得商机状态列表")
     @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java
index eec9de8b0..401e35fbe 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java
@@ -12,6 +12,8 @@ import javax.validation.constraints.NotNull;
 @Data
 public class CrmBusinessStatusBaseVO {
 
+    // TODO @lilleo:example 要写下
+
     @Schema(description = "状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22882")
     @NotNull(message = "状态类型编号不能为空")
     private Long typeId;
@@ -20,9 +22,11 @@ public class CrmBusinessStatusBaseVO {
     @NotNull(message = "状态名不能为空")
     private String name;
 
+    // TODO @lilleo:percent 应该是 Integer;
     @Schema(description = "赢单率")
     private String percent;
 
+    // TODO @lilleo:这个是不是不用前端新增和修改的时候传递,交给顺序计算出来,存储起来就好了;
     @Schema(description = "排序")
     private Integer sort;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java
index 83002469e..78da092f7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java
@@ -1,11 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
 
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-
 import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
 
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
 /**
  * 商机状态 Excel VO
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java
index 3fecb03ee..7f7fba6c7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java
@@ -1,10 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
 
-import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import lombok.Data;
 
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
 @Schema(description = "管理后台 - 商机状态 Excel 导出 Request VO,参数和 CrmBusinessStatusPageReqVO 是一致的")
 @Data
 public class CrmBusinessStatusExportReqVO {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java
index 2a0fa07a4..54f675272 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java
@@ -9,7 +9,7 @@ import lombok.*;
 @ToString(callSuper = true)
 public class CrmBusinessStatusRespVO extends CrmBusinessStatusBaseVO {
 
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "6802")
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6802")
     private Long id;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java
index 11f403330..429902164 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 商机状态更新 Request VO")
 @Data
@@ -11,8 +13,8 @@ import javax.validation.constraints.*;
 @ToString(callSuper = true)
 public class CrmBusinessStatusUpdateReqVO extends CrmBusinessStatusBaseVO {
 
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "6802")
-    @NotNull(message = "主键不能为空")
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6802")
+    @NotNull(message = "编号不能为空")
     private Long id;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java
index ae0c55d62..25ba7448f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java
@@ -26,6 +26,7 @@ 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;
 
+// TODO @lilleo:这个模块,可以挪到 business 下;这样我打开 business 包下,就知道,噢~原来里面有 business 商机、有 type 状态类型、status 具体状态;
 @Tag(name = "管理后台 - 商机状态类型")
 @RestController
 @RequestMapping("/crm/business-status-type")
@@ -68,6 +69,7 @@ public class CrmBusinessStatusTypeController {
         return success(CrmBusinessStatusTypeConvert.INSTANCE.convert(businessStatusType));
     }
 
+    // TODO @lilleo:这个接口,暂时用不到,可以考虑先删除掉
     @GetMapping("/list")
     @Operation(summary = "获得商机状态类型列表")
     @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java
index e9f958a6c..5000e25ee 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java
@@ -5,6 +5,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
+// TODO 状态类型和状态添加,是在一个请求里,所以需要把 CrmBusinessStatusCreateReqVO 融合进来;
 @Schema(description = "管理后台 - 商机状态类型创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java
index ea82616de..cc6ed8502 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java
@@ -1,13 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
 
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-
 import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
 
+import java.time.LocalDateTime;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
 /**
  * 商机状态类型 Excel VO
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java
index 48d5bc7ed..1345565be 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java
@@ -1,14 +1,14 @@
 package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
 
-import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import java.time.LocalDateTime;
+import lombok.Data;
 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;
 
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
 @Schema(description = "管理后台 - 商机状态类型 Excel 导出 Request VO,参数和 CrmBusinessStatusTypePageReqVO 是一致的")
 @Data
 public class CrmBusinessStatusTypeExportReqVO {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java
index 5fab5fc2f..0eb93224c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java
@@ -1,10 +1,13 @@
 package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
 
+import javax.validation.constraints.NotNull;
+
+// TODO 状态类型和状态添加,是在一个请求里,所以需要把 CrmBusinessStatusUpdateReqVO 融合进来;
 @Schema(description = "管理后台 - 商机状态类型更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatus/CrmBusinessStatusDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatus/CrmBusinessStatusDO.java
index a60c6e40d..3a1b66ad0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatus/CrmBusinessStatusDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatus/CrmBusinessStatusDO.java
@@ -26,6 +26,8 @@ public class CrmBusinessStatusDO {
     private Long id;
     /**
      * 状态类型编号
+     *
+     * // TODO @ljlleo:要写下关联字段噢
      */
     private Long typeId;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatustype/CrmBusinessStatusTypeDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatustype/CrmBusinessStatusTypeDO.java
index 4a2005e49..fbc2e6857 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatustype/CrmBusinessStatusTypeDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/businessstatustype/CrmBusinessStatusTypeDO.java
@@ -28,6 +28,7 @@ public class CrmBusinessStatusTypeDO {
      * 状态类型名
      */
     private String name;
+    // TODO @ljlleo:List 存储哈
     /**
      * 使用的部门编号
      */
@@ -35,6 +36,7 @@ public class CrmBusinessStatusTypeDO {
     /**
      * 开启状态
      */
+    // TODO @ljlleo:这个字段,使用 Integer,对应 CommonStatus
     private Boolean status;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatustype/CrmBusinessStatusTypeMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatustype/CrmBusinessStatusTypeMapper.java
index bc23b2e8b..258477e4d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatustype/CrmBusinessStatusTypeMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/businessstatustype/CrmBusinessStatusTypeMapper.java
@@ -21,7 +21,7 @@ public interface CrmBusinessStatusTypeMapper extends BaseMapperX<CrmBusinessStat
     default PageResult<CrmBusinessStatusTypeDO> selectPage(CrmBusinessStatusTypePageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<CrmBusinessStatusTypeDO>()
                 .likeIfPresent(CrmBusinessStatusTypeDO::getName, reqVO.getName())
-                .eqIfPresent(CrmBusinessStatusTypeDO::getDeptIds, reqVO.getDeptIds())
+//                .eqIfPresent(CrmBusinessStatusTypeDO::getDeptIds, reqVO.getDeptIds()) TODO 报错,临时注释掉
                 .eqIfPresent(CrmBusinessStatusTypeDO::getStatus, reqVO.getStatus())
                 .orderByDesc(CrmBusinessStatusTypeDO::getId));
     }
@@ -38,6 +38,4 @@ public interface CrmBusinessStatusTypeMapper extends BaseMapperX<CrmBusinessStat
         return selectList(CrmBusinessStatusTypeDO::getStatus, status.byteValue());
     }
 
-
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusService.java
index a0dadcd17..44b3e9f1b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusService.java
@@ -73,9 +73,9 @@ public interface CrmBusinessStatusService {
     List<CrmBusinessStatusDO> getBusinessStatusList(CrmBusinessStatusExportReqVO exportReqVO);
 
     /**
-     * 根据类型ID获得商机状态列表
+     * 根据类型 ID 获得商机状态列表
      *
-     * @param typeId 商机状态类型ID
+     * @param typeId 商机状态类型 ID
      * @return 商机状态列表
      */
     List<CrmBusinessStatusDO> getBusinessStatusListByTypeId(Integer typeId);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImpl.java
index 88b25ad1e..f428c3836 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImpl.java
@@ -34,6 +34,7 @@ public class CrmBusinessStatusTypeServiceImpl implements CrmBusinessStatusTypeSe
 
     @Override
     public Long createBusinessStatusType(CrmBusinessStatusTypeCreateReqVO createReqVO) {
+        // TODO ljlleo:name 应该需要唯一哈;
         // 插入
         CrmBusinessStatusTypeDO businessStatusType = CrmBusinessStatusTypeConvert.INSTANCE.convert(createReqVO);
         businessStatusTypeMapper.insert(businessStatusType);
@@ -43,6 +44,7 @@ public class CrmBusinessStatusTypeServiceImpl implements CrmBusinessStatusTypeSe
 
     @Override
     public void updateBusinessStatusType(CrmBusinessStatusTypeUpdateReqVO updateReqVO) {
+        // TODO ljlleo:name 应该需要唯一哈;
         // 校验存在
         validateBusinessStatusTypeExists(updateReqVO.getId());
         // 更新
@@ -54,6 +56,7 @@ public class CrmBusinessStatusTypeServiceImpl implements CrmBusinessStatusTypeSe
     public void deleteBusinessStatusType(Long id) {
         // 校验存在
         validateBusinessStatusTypeExists(id);
+        // TODO 艿艿:这里在看看,是不是要校验业务是否在使用;
         // 删除
         businessStatusTypeMapper.deleteById(id);
     }

From 301f497527edcc8a594dbc8fe79a91ab8eeec079 Mon Sep 17 00:00:00 2001
From: liuhongfeng <291117974@qq.com>
Date: Sat, 28 Oct 2023 00:37:47 +0800
Subject: [PATCH 027/101] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91?=
 =?UTF-8?q?=E5=9B=9E=E6=AC=BE=E8=AE=A1=E5=88=92-=E5=AE=A1=E6=89=B9?=
 =?UTF-8?q?=E6=9E=9A=E4=B8=BE=E7=B1=BB=20=E3=80=90=E6=96=B0=E5=A2=9E?=
 =?UTF-8?q?=E3=80=91=E5=9B=9E=E6=AC=BE=E8=AE=A1=E5=88=92-=E5=9B=9E?=
 =?UTF-8?q?=E6=AC=BE=E6=96=B9=E5=BC=8F=E6=9E=9A=E4=B8=BE=E7=B1=BB=20?=
 =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91=E5=9B=9E=E6=AC=BE=E8=AE=A1?=
 =?UTF-8?q?=E5=88=92-=E6=9E=9A=E4=B8=BE=E7=B1=BB=E6=A0=A1=E9=AA=8C=20?=
 =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E3=80=91=E5=9B=9E=E6=AC=BE=E8=AE=A1?=
 =?UTF-8?q?=E5=88=92-=E9=94=99=E8=AF=AF=E7=A0=81=E8=B0=83=E6=95=B4=20?=
 =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E3=80=91=E4=BF=AE=E6=94=B9=E5=9B=9E?=
 =?UTF-8?q?=E6=AC=BE=E8=AE=A1=E5=88=92-=E5=88=9B=E5=BB=BA=E9=80=BB?=
 =?UTF-8?q?=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=90=88=E5=90=8C=E6=95=B0?=
 =?UTF-8?q?=E6=8D=AE=E7=9A=84=E6=A0=A1=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/AuditStatusEnum.java     | 61 +++++++++++++++++++
 .../module/crm/enums/ErrorCodeConstants.java  |  7 ++-
 .../module/crm/enums/ReturnTypeEnum.java      |  8 +++
 .../admin/receivable/vo/ReceivableBaseVO.java |  5 +-
 .../receivable/ReceivableServiceImpl.java     | 26 ++++++++
 5 files changed, 102 insertions(+), 5 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
new file mode 100644
index 000000000..ce04c18a1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+
+import java.util.Arrays;
+
+/**
+ * 流程审批状态枚举类
+ * 0 未审核 1 审核通过 2 审核拒绝 3 审核中 4 已撤回
+ * @author 赤焰
+ */
+public enum AuditStatusEnum implements IntArrayValuable {
+    /**
+     * 未审批
+     */
+    AUDIT_NEW(0, "未审批"),
+    /**
+     * 审核通过
+     */
+	AUDIT_FINISH(0, "审核通过"),
+    /**
+     * 审核拒绝
+     */
+	AUDIT_REJECT(2, "审核拒绝"),
+    /**
+     * 审核中
+     */
+    AUDIT_DOING(3, "审核中"),
+	/**
+	 * 已撤回
+	 */
+	AUDIT_RETURN(4, "已撤回");
+
+    private final Integer value;
+    private final String desc;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AuditStatusEnum::getValue).toArray();
+
+    /**
+     *
+     * @param value
+     * @param desc
+     */
+    AuditStatusEnum(Integer value, String desc) {
+        this.value = value;
+        this.desc = desc;
+    }
+
+    public Integer getValue() {
+        return value;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 07d9b6d6a..ea0f2d51f 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -21,9 +21,10 @@ public interface ErrorCodeConstants {
     // ========== 联系人管理 1-020-003-000 ==========
     ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
 
-    // TODO @liuhongfeng:错误码分段;
-    ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_030_000_001, "回款管理不存在");
+    // ========== 回款管理 1-020-004-000 ==========
+    ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_020_004_000, "回款管理不存在");
 
-    ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_040_000_001, "回款计划不存在");
+    // ========== 合同管理 1-020-005-000 ==========
+    ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_020_005_000, "回款计划不存在");
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java
new file mode 100644
index 000000000..d9ca477be
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java
@@ -0,0 +1,8 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+/**
+ * @author 赤焰
+ */
+
+public enum ReturnTypeEnum {
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
index bb0bc5745..8aa43e50b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.AuditStatusEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
@@ -33,9 +35,8 @@ public class ReceivableBaseVO {
     @Schema(description = "合同ID", example = "30305")
     private Long contractId;
 
-    // TODO @liuhongfeng:这个字段,可以写个枚举,然后 InEnum 去校验下;
-    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的
     @Schema(description = "审批状态", example = "1")
+    @InEnum(AuditStatusEnum.class)
     private Integer checkStatus;
 
     // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
index 94b25f6ed..39de05fe1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
@@ -2,14 +2,19 @@ package cn.iocoder.yudao.module.crm.service.receivable;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableExportReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
+import cn.iocoder.yudao.module.crm.enums.AuditStatusEnum;
+import cn.iocoder.yudao.module.crm.service.contract.ContractService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -18,6 +23,7 @@ import java.util.Collection;
 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.ErrorCodeConstants.RECEIVABLE_NOT_EXISTS;
 
 /**
@@ -31,6 +37,8 @@ public class ReceivableServiceImpl implements ReceivableService {
 
     @Resource
     private ReceivableMapper receivableMapper;
+    @Resource
+    private ContractService contractService;
 
     @Override
     public Long createReceivable(ReceivableCreateReqVO createReqVO) {
@@ -38,11 +46,29 @@ public class ReceivableServiceImpl implements ReceivableService {
         // TODO @liuhongfeng:其它类似 customerId、contractId 也需要去校验;
         // 插入
         ReceivableDO receivable = ReceivableConvert.INSTANCE.convert(createReqVO);
+
+        receivable.setCheckStatus(AuditStatusEnum.AUDIT_NEW.getValue());
+        //校验
+        checkReceivable(receivable);
+
         receivableMapper.insert(receivable);
         // 返回
         return receivable.getId();
     }
 
+    private void checkReceivable(ReceivableDO receivable) {
+
+        if(ObjectUtil.isNull(receivable.getContractId())){
+            throw exception(CONTRACT_NOT_EXISTS);
+        }
+
+        ContractDO contract = contractService.getContract(receivable.getContractId());
+        if(ObjectUtil.isNull(contract)){
+            throw exception(CONTRACT_NOT_EXISTS);
+        }
+
+    }
+
     @Override
     public void updateReceivable(ReceivableUpdateReqVO updateReqVO) {
         // 校验存在

From c18f5baa4b35d45161439f6368f16c2c9a0df954 Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Sat, 28 Oct 2023 22:47:56 +0800
Subject: [PATCH 028/101] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E8=A1=A8=E7=9A=84=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  2 -
 .../admin/customer/CrmCustomerController.java |  1 -
 .../admin/customer/vo/CrmCustomerBaseVO.java  | 46 ++++++++-----------
 .../customer/vo/CrmCustomerCreateReqVO.java   |  8 ----
 .../admin/customer/vo/CrmCustomerExcelVO.java | 18 +++++---
 .../customer/vo/CrmCustomerExportReqVO.java   |  6 ---
 .../customer/vo/CrmCustomerPageReqVO.java     |  8 ----
 .../admin/customer/vo/CrmCustomerRespVO.java  | 16 +++++++
 .../customer/vo/CrmCustomerUpdateReqVO.java   |  7 ---
 .../dataobject/customer/CrmCustomerDO.java    | 37 +++++++++------
 .../dal/mysql/customer/CrmCustomerMapper.java |  4 --
 .../crm/service/clue/CrmClueServiceImpl.java  | 16 +------
 .../service/customer/CrmCustomerService.java  |  8 ++++
 .../customer/CrmCustomerServiceImpl.java      | 14 ++++++
 .../src/test/resources/sql/create_tables.sql  |  2 -
 15 files changed, 95 insertions(+), 98 deletions(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 95a81df8e..1b381bf04 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -203,8 +203,6 @@ CREATE TABLE `crm_customer` (
     `rw_user_ids` varchar(4096) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '读写权限的用户编号数组',
     `area_id` bigint DEFAULT NULL COMMENT '地区编号',
     `detail_address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '详细地址',
-    `longitude` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地理位置经度',
-    `latitude` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地理位置维度',
     `contact_last_time` datetime DEFAULT NULL COMMENT '最后跟进时间',
     `contact_next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
     `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 7c91b4c1a..78ae1a20d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -19,7 +19,6 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.IOException;
-import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
index 0db54fa1f..fd061a39e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
@@ -6,8 +6,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
+import javax.validation.constraints.Email;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -23,16 +25,6 @@ public class CrmCustomerBaseVO {
     @NotEmpty(message = "客户名称不能为空")
     private String name;
 
-    // TODO wanwan:这个字段应该只有 RespVO 会有;创建和修改不传递;
-    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
-    @NotNull(message = "跟进状态不能为空")
-    private Boolean followUpStatus;
-
-    // TODO wanwan:这个字段应该只有 RespVO 会有;创建和修改不传递;
-    @Schema(description = "锁定状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
-    @NotNull(message = "锁定状态不能为空")
-    private Boolean lockStatus;
-
     @Schema(description = "手机", example = "18000000000")
     @Mobile
     private String mobile;
@@ -44,32 +36,32 @@ public class CrmCustomerBaseVO {
     @Schema(description = "网址", example = "https://www.baidu.com")
     private String website;
 
+    @Schema(description = "QQ", example = "123456789")
+    @Size(max = 20, message = "QQ长度不能超过20个字符")
+    private String qq;
+
+    @Schema(description = "wechat", example = "123456789")
+    @Size(max = 255, message = "微信长度不能超过255个字符")
+    private String wechat;
+
+    @Schema(description = "email", example = "123456789@qq.com")
+    @Email(message = "邮箱格式不正确")
+    @Size(max = 255, message = "邮箱长度不能超过255个字符")
+    private String email;
+
+    @Schema(description = "客户描述", example = "任意文字")
+    @Size(max = 4096, message = "客户描述长度不能超过255个字符")
+    private String description;
+
     @Schema(description = "备注", example = "随便")
     private String remark;
 
-    // TODO wanwan:这个字段应该只有 RespVO 会有;创建和修改不传递;因为它会在“移交”里面做哈
-    @Schema(description = "负责人的用户编号", example = "25682")
-    @NotNull(message = "负责人不能为空")
-    private Long ownerUserId;
-
     @Schema(description = "地区编号", example = "20158")
     private Long areaId;
 
     @Schema(description = "详细地址", example = "北京市海淀区")
     private String detailAddress;
 
-    // TODO @芋艿:longitude、latitude 这两个字段删除;
-    @Schema(description = "地理位置经度", example = "116.40341")
-    private String longitude;
-
-    @Schema(description = "地理位置维度", example = "39.92409")
-    private String latitude;
-
-    // TODO wanwan:这个字段应该只有 RespVO 会有;创建和修改不传递;
-    @Schema(description = "最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime contactLastTime;
-
     @Schema(description = "下次联系时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactNextTime;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
index a3ed7ef13..ab806b689 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
@@ -11,12 +11,4 @@ import lombok.ToString;
 @ToString(callSuper = true)
 public class CrmCustomerCreateReqVO extends CrmCustomerBaseVO {
 
-    // TODO @wanwan:类型应该是传递 List<Long>; 不过这个字段,默认新建的时候不传递,在“移交”功能里管理
-    @Schema(description = "只读权限的用户编号数组")
-    private String roUserIds;
-
-    // TODO @wanwan:类型应该是传递 List<Long>; 不过这个字段,默认新建的时候不传递,在“移交”功能里管理
-    @Schema(description = "读写权限的用户编号数组")
-    private String rwUserIds;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
index 3e37c30e7..35b864080 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
@@ -44,6 +44,18 @@ public class CrmCustomerExcelVO {
     @ExcelProperty("网址")
     private String website;
 
+    @ExcelProperty("QQ")
+    private String qq;
+
+    @ExcelProperty("wechat")
+    private String wechat;
+
+    @ExcelProperty("email")
+    private String email;
+
+    @ExcelProperty("客户描述")
+    private String description;
+
     @ExcelProperty("备注")
     private String remark;
 
@@ -56,12 +68,6 @@ public class CrmCustomerExcelVO {
     @ExcelProperty("详细地址")
     private String detailAddress;
 
-    @ExcelProperty("地理位置经度")
-    private String longitude;
-
-    @ExcelProperty("地理位置维度")
-    private String latitude;
-
     @ExcelProperty("最后跟进时间")
     private LocalDateTime contactLastTime;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
index 15776aa42..03a3f2d4d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
@@ -14,10 +14,4 @@ public class CrmCustomerExportReqVO {
     @Schema(description = "手机", example = "18000000000")
     private String mobile;
 
-    @Schema(description = "电话", example = "18000000000")
-    private String telephone;
-
-    @Schema(description = "网址", example = "https://www.baidu.com")
-    private String website;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
index 7cb89adbc..8bf2018f1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
@@ -18,13 +18,5 @@ public class CrmCustomerPageReqVO extends PageParam {
     @Schema(description = "手机", example = "18000000000")
     private String mobile;
 
-    // TODO @wanwan:这个字段不需要哈
-    @Schema(description = "电话", example = "18000000000")
-    private String telephone;
-
-    // TODO @wanwan:这个字段不需要哈
-    @Schema(description = "网址", example = "https://www.baidu.com")
-    private String website;
-
     // TODO @芋艿:场景;
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
index b5c9850bc..bdcf28f2c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
@@ -4,9 +4,12 @@ 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 = "管理后台 - 客户 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -16,9 +19,22 @@ public class CrmCustomerRespVO extends CrmCustomerBaseVO {
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     private Long id;
 
+    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean followUpStatus;
+
+    @Schema(description = "锁定状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean lockStatus;
+
     @Schema(description = "成交状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     private Boolean dealStatus;
 
+    @Schema(description = "负责人的用户编号", example = "25682")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
index 59d440a35..545643fd9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
@@ -17,11 +17,4 @@ public class CrmCustomerUpdateReqVO extends CrmCustomerBaseVO {
     @NotNull(message = "编号不能为空")
     private Long id;
 
-    // TODO @wanwan:下面两个字段,同 CrmCustomerCreateReqVO
-    @Schema(description = "只读权限的用户编号数组")
-    private String roUserIds;
-
-    @Schema(description = "读写权限的用户编号数组")
-    private String rwUserIds;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index 101d0dd27..bb45679bb 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -1,12 +1,15 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.customer;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
 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.*;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 // TODO 芋艿:调整下字段
 /**
@@ -14,7 +17,7 @@ import java.time.LocalDateTime;
  *
  * @author Wanwan
  */
-@TableName("crm_customer")
+@TableName(value = "crm_customer", autoResultMap = true)
 @KeySequence("crm_customer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -57,6 +60,22 @@ public class CrmCustomerDO extends BaseDO {
      * 网址
      */
     private String website;
+    /**
+     * QQ
+     */
+    private String qq;
+    /**
+     * wechat
+     */
+    private String wechat;
+    /**
+     * email
+     */
+    private String email;
+    /**
+     * 客户描述
+     */
+    private String description;
     /**
      * 备注
      */
@@ -65,15 +84,16 @@ public class CrmCustomerDO extends BaseDO {
      * 负责人的用户编号
      */
     private Long ownerUserId;
-    // TODO @wanwan:下面两个字段,使用 List<Long>,然后使用 typeHandler = LongListTypeHandler 解决持久化的问题;注意类上需要加 autoResultMap = true
     /**
      * 只读权限的用户编号数组
      */
-    private String roUserIds;
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> roUserIds;
     /**
      * 读写权限的用户编号数组
      */
-    private String rwUserIds;
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> rwUserIds;
     /**
      * 地区编号
      */
@@ -82,15 +102,6 @@ public class CrmCustomerDO extends BaseDO {
      * 详细地址
      */
     private String detailAddress;
-    // TODO @wanwan:下面两个字段:删除
-    /**
-     * 地理位置经度
-     */
-    private String longitude;
-    /**
-     * 地理位置维度
-     */
-    private String latitude;
     /**
      * 最后跟进时间
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
index 647b6d812..bc684d7bc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -22,8 +22,6 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
         return selectPage(reqVO, new LambdaQueryWrapperX<CrmCustomerDO>()
                 .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
                 .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
-                .eqIfPresent(CrmCustomerDO::getTelephone, reqVO.getTelephone())
-                .likeIfPresent(CrmCustomerDO::getWebsite, reqVO.getWebsite())
                 .orderByDesc(CrmCustomerDO::getId));
     }
 
@@ -31,8 +29,6 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
         return selectList(new LambdaQueryWrapperX<CrmCustomerDO>()
                 .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
                 .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
-                .eqIfPresent(CrmCustomerDO::getTelephone, reqVO.getTelephone())
-                .likeIfPresent(CrmCustomerDO::getWebsite, reqVO.getWebsite())
                 .orderByDesc(CrmCustomerDO::getId));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
index 32577dd24..362637e30 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
@@ -39,7 +39,7 @@ public class CrmClueServiceImpl implements CrmClueService {
     @Override
     public Long createClue(CrmClueCreateReqVO createReqVO) {
         // 校验客户是否存在
-        validateCustomerExists(createReqVO.getCustomerId());
+        customerService.validateCustomer(createReqVO.getCustomerId());
         // 插入
         CrmClueDO clue = CrmClueConvert.INSTANCE.convert(createReqVO);
         clueMapper.insert(clue);
@@ -52,7 +52,7 @@ public class CrmClueServiceImpl implements CrmClueService {
         // 校验存在
         validateClueExists(updateReqVO.getId());
         // 校验客户是否存在
-        validateCustomerExists(updateReqVO.getCustomerId());
+        customerService.validateCustomer(updateReqVO.getCustomerId());
 
         // 更新
         CrmClueDO updateObj = CrmClueConvert.INSTANCE.convert(updateReqVO);
@@ -96,16 +96,4 @@ public class CrmClueServiceImpl implements CrmClueService {
         return clueMapper.selectList(exportReqVO);
     }
 
-    // TODO @wanwan:可以在 CrmClueServiceImpl 中,增加一个方法,用于校验客户是否存在;validateCustomer;然后其它方法可以调用它。不过要注意,需要把 CustomerDO 返回,因为其它模块可能要它的信息
-    /**
-     * 校验客户是否存在
-     *
-     * @param customerId 客户id
-     */
-    private void validateCustomerExists(Long customerId) {
-        if (customerService.getCustomer(customerId) == null) {
-            throw exception(CUSTOMER_NOT_EXISTS);
-        }
-    }
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index c2190ff94..4e41fab25 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -72,4 +72,12 @@ public interface CrmCustomerService {
      */
     List<CrmCustomerDO> getCustomerList(CrmCustomerExportReqVO exportReqVO);
 
+    /**
+     * 校验客户是否存在
+     *
+     * @param customerId 客户id
+     * @return
+     */
+    CrmCustomerDO validateCustomer(Long customerId);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 0bb224e7f..d77953231 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -85,4 +85,18 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         return customerMapper.selectList(exportReqVO);
     }
 
+    /**
+     * 校验客户是否存在
+     *
+     * @param customerId 客户id
+     * @return
+     */
+    @Override
+    public CrmCustomerDO validateCustomer(Long customerId) {
+        CrmCustomerDO customer = getCustomer(customerId);
+        if (Objects.isNull(customer)) {
+            throw exception(CUSTOMER_NOT_EXISTS);
+        }
+        return customer;
+    }
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
index f7d25f7ef..124edb883 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -113,8 +113,6 @@ CREATE TABLE IF NOT EXISTS "crm_customer" (
   "rw_user_ids" varchar,
   "area_id" bigint,
   "detail_address" varchar,
-  "longitude" varchar,
-  "latitude" varchar,
   "contact_last_time" varchar,
   "contact_next_time" varchar,
   "creator" varchar DEFAULT '',

From 3af9688383ea5aa48d727fefe4cc39917b239555 Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Sun, 29 Oct 2023 00:33:42 +0800
Subject: [PATCH 029/101] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E8=A1=A8=E7=9A=84=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             | 62 +++++++++++--------
 sql/mysql/crm_data.sql                        | 39 ++++++++++++
 .../module/crm/enums/DictTypeConstants.java   | 15 +++++
 .../enums/customer/CrmCustomerLevelEnum.java  | 38 ++++++++++++
 .../admin/customer/vo/CrmCustomerBaseVO.java  | 12 ++++
 .../admin/customer/vo/CrmCustomerExcelVO.java | 13 ++++
 .../dataobject/customer/CrmCustomerDO.java    | 16 +++++
 .../dal/mysql/customer/CrmCustomerMapper.java |  1 +
 .../customer/CrmCustomerServiceImpl.java      |  3 +
 9 files changed, 173 insertions(+), 26 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 1b381bf04..51b57412a 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -188,32 +188,42 @@ CREATE TABLE `crm_contact` (
 -- ----------------------------
 -- 客户表
 -- ----------------------------
-CREATE TABLE `crm_customer` (
-    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
-    `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户名称',
-    `follow_up_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '跟进状态',
-    `lock_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '锁定状态',
-    `deal_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '成交状态',
-    `mobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机',
-    `telephone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
-    `website` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '网址',
-    `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
-    `owner_user_id` bigint DEFAULT NULL COMMENT '负责人的用户编号',
-    `ro_user_ids` varchar(4096) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '只读权限的用户编号数组',
-    `rw_user_ids` varchar(4096) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '读写权限的用户编号数组',
-    `area_id` bigint DEFAULT NULL COMMENT '地区编号',
-    `detail_address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '详细地址',
-    `contact_last_time` datetime DEFAULT NULL COMMENT '最后跟进时间',
-    `contact_next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
-    `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
-    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
-    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-    `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
-    PRIMARY KEY (`id`),
-    KEY `owner_user_id` (`owner_user_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户表';
+DROP TABLE IF EXISTS `crm_customer`;
+CREATE TABLE `crm_customer`  (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户名称',
+ `follow_up_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '跟进状态',
+ `lock_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '锁定状态',
+ `deal_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '成交状态',
+ `industry_id` int NULL DEFAULT NULL COMMENT '所属行业',
+ `level` int NULL DEFAULT NULL COMMENT '客户等级',
+ `source` int NULL DEFAULT NULL COMMENT '客户来源',
+ `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机',
+ `telephone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电话',
+ `website` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '网址',
+ `qq` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'QQ',
+ `wechat` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信',
+ `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
+ `description` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户描述',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
+ `owner_user_id` bigint NULL DEFAULT NULL COMMENT '负责人的用户编号',
+ `ro_user_ids` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '只读权限的用户编号数组',
+ `rw_user_ids` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '读写权限的用户编号数组',
+ `area_id` bigint NULL DEFAULT NULL COMMENT '地区编号',
+ `detail_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '详细地址',
+ `longitude` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地理位置经度',
+ `latitude` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地理位置维度',
+ `contact_last_time` datetime NULL DEFAULT NULL COMMENT '最后跟进时间',
+ `contact_next_time` datetime NULL DEFAULT NULL COMMENT '下次联系时间',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+ `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `owner_user_id`(`owner_user_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客户表' ROW_FORMAT = Dynamic;
 -- ----------------------------
 -- 商机状态表
 -- ----------------------------
diff --git a/sql/mysql/crm_data.sql b/sql/mysql/crm_data.sql
index dfc1135f9..63b7d9f72 100644
--- a/sql/mysql/crm_data.sql
+++ b/sql/mysql/crm_data.sql
@@ -17,3 +17,42 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1400, 7, '微信支付', '7', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:53', '1', '2023-10-18 21:55:53', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1401, 8, '其他', '8', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:56:06', '1', '2023-10-18 21:56:06', b'0');
 
+
+
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (188, '客户来源', 'crm_customer_source', 0, 'CRM 客户来源', '1', '2023-10-28 23:00:34', '1', '2023-10-28 15:11:16', b'0', NULL);
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (187, '客户等级', 'crm_customer_level', 0, 'CRM 客户等级', '1', '2023-10-28 22:59:12', '1', '2023-10-28 15:11:16', b'0', NULL);
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (186, '客户所属行业', 'crm_customer_industry', 0, 'CRM 客户所属行业', '1', '2023-10-28 22:57:07', '1', '2023-10-28 15:11:16', b'0', NULL);
+
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1434, 10, '邮件咨询', '10', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:33', '1', '2023-10-28 23:10:33', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1433, 9, '电话咨询', '9', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:18', '1', '2023-10-28 23:10:18', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1432, 8, '陌拜', '8', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:04', '1', '2023-10-28 23:10:04', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1431, 7, '预约上门', '7', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:39', '1', '2023-10-28 23:09:39', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1430, 6, '线上咨询', '6', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:22', '1', '2023-10-28 23:09:22', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1429, 5, '线上注册', '5', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:12', '1', '2023-10-28 23:09:12', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1428, 4, '转介绍', '4', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:58', '1', '2023-10-28 23:08:58', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1427, 3, '广告', '3', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:47', '1', '2023-10-28 23:08:47', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1426, 2, '搜索引擎', '2', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:39', '1', '2023-10-28 23:08:39', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1425, 1, '促销', '1', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:29', '1', '2023-10-28 23:08:29', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1424, 3, 'C (非优先客户)', '3', 'crm_customer_level', 0, 'default', '', '', '1', '2023-10-28 23:07:53', '1', '2023-10-28 23:07:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1423, 2, 'B (普通客户)', '2', 'crm_customer_level', 0, 'info', '', '', '1', '2023-10-28 23:07:35', '1', '2023-10-28 23:07:35', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1422, 1, 'A (重点客户)', '1', 'crm_customer_level', 0, 'primary', '', '', '1', '2023-10-28 23:07:13', '1', '2023-10-28 23:07:13', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1421, 20, 'T 国际组织', '20', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:06:15', '1', '2023-10-28 23:06:15', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1420, 19, 'S 公共管理、社会保障和社会组织', '19', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:06:05', '1', '2023-10-28 23:06:05', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1419, 18, 'R 文化、体育和娱乐业', '18', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:55', '1', '2023-10-28 23:05:55', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1418, 17, 'Q 卫生和社会工作', '17', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:44', '1', '2023-10-28 23:05:44', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1417, 16, 'P 教育', '16', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:15', '1', '2023-10-28 23:05:15', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1416, 15, 'O 居民服务、修理和其他服务业', '15', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:05', '1', '2023-10-28 23:05:05', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1415, 14, 'N 水利、环境和公共设施管理业', '14', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:53', '1', '2023-10-28 23:04:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1414, 13, 'M 科学研究和技术服务业', '13', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:43', '1', '2023-10-28 23:04:43', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1413, 12, 'L 租赁和商务服务业', '12', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:33', '1', '2023-10-28 23:04:33', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1412, 11, 'K 房地产业', '11', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:15', '1', '2023-10-28 23:04:22', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1411, 10, 'J 金融业', '10', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:57', '1', '2023-10-28 23:03:57', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1410, 9, 'I 信息传输、软件和信息技术服务业', '9', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:47', '1', '2023-10-28 23:03:47', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1409, 8, 'H 住宿和餐饮业', '8', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:37', '1', '2023-10-28 23:03:37', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1408, 7, 'G 交通运输、仓储和邮政业', '7', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:27', '1', '2023-10-28 23:03:27', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1407, 6, 'F 批发和零售业', '6', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:13', '1', '2023-10-28 23:03:13', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1406, 5, 'E 建筑业', '5', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:03', '1', '2023-10-28 23:03:03', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1405, 4, 'D 电力、热力、燃气及水生产和供应业', '4', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:54', '1', '2023-10-28 23:02:54', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1404, 3, 'C 制造业', '3', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:41', '1', '2023-10-28 23:02:41', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1403, 2, 'B 采矿业', '2', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:29', '1', '2023-10-28 23:02:29', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1402, 1, 'A 农、林、牧、渔业', '1', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:15', '1', '2023-10-28 23:02:15', b'0');
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
new file mode 100644
index 000000000..dd2c068d3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+/**
+ * System 字典类型的枚举类
+ *
+ * @author 芋道源码
+ */
+public interface DictTypeConstants {
+
+    // ========== CRM 模块 ==========
+    String CRM_CUSTOMER_INDUSTRY = "crm_customer_industry"; // CRM 客户所属行业
+    String CRM_CUSTOMER_LEVEL = "crm_customer_level"; // CRM 客户等级
+    String CRM_CUSTOMER_SOURCE = "crm_customer_source"; // CRM 客户来源
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
new file mode 100644
index 000000000..c2da3744e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.enums.customer;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * CRM 客户等级
+ *
+ * @author Wanwan
+ */
+@Getter
+@AllArgsConstructor
+public enum CrmCustomerLevelEnum implements IntArrayValuable {
+
+    IMPORTANT(1, "A (重点客户)"),
+    GENERAL(2, "B (普通客户)"),
+    LOW_PRIORITY(3, "C (非优先客户)");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLevelEnum::getStatus).toArray();
+
+    /**
+     * 状态
+     */
+    private final Integer status;
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
index fd061a39e..f3b81151c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
@@ -1,7 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
 
+import cn.iocoder.yudao.framework.common.validation.InEnum;
 import cn.iocoder.yudao.framework.common.validation.Mobile;
 import cn.iocoder.yudao.framework.common.validation.Telephone;
+import cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLevelEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
@@ -25,6 +27,16 @@ public class CrmCustomerBaseVO {
     @NotEmpty(message = "客户名称不能为空")
     private String name;
 
+    @Schema(description = "所属行业", example = "1")
+    private Integer industryId;
+
+    @Schema(description = "客户等级", example = "2")
+    @InEnum(CrmCustomerLevelEnum.class)
+    private Integer level;
+
+    @Schema(description = "客户来源", example = "3")
+    private Integer source;
+
     @Schema(description = "手机", example = "18000000000")
     @Mobile
     private String mobile;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
index 35b864080..0fee3df4c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
 import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -35,6 +36,18 @@ public class CrmCustomerExcelVO {
     @DictFormat(DictTypeConstants.BOOLEAN_STRING)
     private Boolean dealStatus;
 
+    @ExcelProperty(value = "所属行业", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY)
+    private Integer industryId;
+
+    @ExcelProperty(value = "客户等级", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL)
+    private Integer level;
+
+    @ExcelProperty(value = "客户来源", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE)
+    private Integer source;
+
     @ExcelProperty("手机")
     private String mobile;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index bb45679bb..a50ada4c5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -12,6 +12,7 @@ import java.time.LocalDateTime;
 import java.util.List;
 
 // TODO 芋艿:调整下字段
+
 /**
  * 客户 DO
  *
@@ -48,6 +49,21 @@ public class CrmCustomerDO extends BaseDO {
      * 成交状态
      */
     private Boolean dealStatus;
+    /**
+     * 所属行业
+     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY}
+     */
+    private Integer industryId;
+    /**
+     * 客户等级
+     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL}
+     */
+    private Integer level;
+    /**
+     * 客户来源
+     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE}
+     */
+    private Integer source;
     /**
      * 手机
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
index bc684d7bc..838423086 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -19,6 +19,7 @@ import java.util.List;
 public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
 
     default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO reqVO) {
+        // TODO @Wanwan 填充负责人,所属部门字段
         return selectPage(reqVO, new LambdaQueryWrapperX<CrmCustomerDO>()
                 .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
                 .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index d77953231..fb27d4ae9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
@@ -29,6 +30,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Resource
     private CrmCustomerMapper customerMapper;
+    @Resource
+    private DeptApi deptApi;
 
     @Override
     public Long createCustomer(CrmCustomerCreateReqVO createReqVO) {

From d9f6fb241a1013c7da355f5d3ef376b44e3788fd Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Sun, 29 Oct 2023 01:23:06 +0800
Subject: [PATCH 030/101] =?UTF-8?q?=E5=90=88=E5=90=8C=EF=BC=9A=E6=96=B0?=
 =?UTF-8?q?=E5=A2=9E=E5=90=88=E5=90=8C=E8=BD=AC=E7=A7=BB=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |  1 +
 .../admin/contract/ContractController.java    | 10 ++++++
 .../contract/vo/ContractCreateReqVO.java      |  6 ++--
 .../contract/vo/ContractUpdateReqVO.java      |  5 +--
 .../contract/vo/CrmContractTransferReqVO.java | 20 +++++++++++
 .../crm/convert/contract/ContractConvert.java | 15 +++++---
 .../dal/dataobject/contract/ContractDO.java   |  9 +++--
 .../module/crm/framework/utils/AuthUtil.java  | 34 +++++++++++++++++++
 .../crm/service/contract/ContractService.java | 13 ++++---
 .../service/contract/ContractServiceImpl.java | 30 ++++++++++++----
 10 files changed, 123 insertions(+), 20 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 124a70cb7..dcb03a74d 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -11,6 +11,7 @@ public interface ErrorCodeConstants {
 
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
+    ErrorCode CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_000_001, "合同转移失败,原因:没有转移权限");
 
     // ========== 线索管理 1-020-001-000 ==========
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
index fd8b14afd..4898eb6c1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
@@ -23,6 +23,7 @@ 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;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 合同")
 @RestController
@@ -86,4 +87,13 @@ public class ContractController {
         ExcelUtils.write(response, "合同.xls", "数据", ContractExcelVO.class, datas);
     }
 
+    @PutMapping("/transfer")
+    @Operation(summary = "合同转移")
+    @PreAuthorize("@ss.hasPermission('crm:contract:update')")
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContractTransferReqVO reqVO) {
+        contractService.contractTransfer(reqVO, getLoginUserId());
+        return success(true);
+    }
+
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
index 38c95d23f..7b8c561b0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
@@ -5,6 +5,8 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
+import java.util.Set;
+
 @Schema(description = "管理后台 - 合同创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -12,9 +14,9 @@ import lombok.ToString;
 public class ContractCreateReqVO extends ContractBaseVO {
 
     @Schema(description = "只读权限的用户编号数组")
-    private String roUserIds;
+    private Set<Long> roUserIds;
 
     @Schema(description = "读写权限的用户编号数组")
-    private String rwUserIds;
+    private Set<Long> rwUserIds;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
index f38ac7677..e6f305ce4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
@@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 import javax.validation.constraints.NotNull;
+import java.util.Set;
 
 @Schema(description = "管理后台 - 合同更新 Request VO")
 @Data
@@ -18,9 +19,9 @@ public class ContractUpdateReqVO extends ContractBaseVO {
     private Long id;
 
     @Schema(description = "只读权限的用户编号数组")
-    private String roUserIds;
+    private Set<Long> roUserIds;
 
     @Schema(description = "读写权限的用户编号数组")
-    private String rwUserIds;
+    private Set<Long> rwUserIds;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
new file mode 100644
index 000000000..5dfa5fe0a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 合同转移 Request VO")
+@Data
+public class CrmContractTransferReqVO {
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "合同编号不能为空")
+    private Long id;
+
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long ownerUserId; // 新的负责人
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index 906dd7562..023d4200b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -1,15 +1,14 @@
 package cn.iocoder.yudao.module.crm.convert.contract;
 
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractExcelVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractUpdateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
+import java.util.Set;
 
 /**
  * 合同 Convert
@@ -33,4 +32,12 @@ public interface ContractConvert {
 
     List<ContractExcelVO> convertList02(List<ContractDO> list);
 
+    default ContractDO convert(ContractDO contract, CrmContractTransferReqVO reqVO, Long userId) {
+        Set<Long> rwUserIds = contract.getRwUserIds();
+        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId.toString())); // 移除老负责人
+        rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
+        return new ContractDO().setId(contract.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
+                .setRwUserIds(rwUserIds);
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
index 8caa4d2cb..2c5d0e44f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
@@ -1,12 +1,15 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contract;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
 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.*;
 
 import java.time.LocalDateTime;
+import java.util.Set;
 
 /**
  * 合同 DO
@@ -79,11 +82,13 @@ public class ContractDO extends BaseDO {
     /**
      * 只读权限的用户编号数组
      */
-    private String roUserIds;
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> roUserIds;
     /**
      * 读写权限的用户编号数组
      */
-    private String rwUserIds;
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> rwUserIds;
     /**
      * 联系人编号
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java
new file mode 100644
index 000000000..3c3d14bf2
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.framework.utils;
+
+import java.util.Collection;
+
+/**
+ * 数据读写权限校验工具类
+ *
+ * @author HUIHUI
+ */
+public class AuthUtil {
+
+    /**
+     * 判断当前数据对用户来说是否是只读的
+     *
+     * @param roUserIds 当前操作数据的只读权限的用户编号数组
+     * @param userId    当前操作数据的用户编号
+     * @return boolean 是/否
+     */
+    public static boolean isReadOnly(Collection<Long> roUserIds, Long userId) {
+        return roUserIds.contains(userId);
+    }
+
+    /**
+     * 判断当前数据对用户来说是否是可读写的
+     *
+     * @param rwUserIds 当前操作数据的读写权限的用户编号数组
+     * @param userId    当前操作数据的用户编号
+     * @return boolean 是/否
+     */
+    public static boolean isReadAndWrite(Collection<Long> rwUserIds, Long userId) {
+        return rwUserIds.contains(userId);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
index 6d6c9fe9f..af8bda185 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
@@ -1,10 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.contract;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractUpdateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 
 import javax.validation.Valid;
@@ -72,4 +69,12 @@ public interface ContractService {
      */
     List<ContractDO> getContractList(ContractExportReqVO exportReqVO);
 
+    /**
+     * 合同转移
+     *
+     * @param reqVO  请求
+     * @param userId 用户编号
+     */
+    void contractTransfer(CrmContractTransferReqVO reqVO, Long userId);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index e840626b9..4dadd6923 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -3,10 +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.module.crm.controller.admin.contract.vo.ContractCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.ContractUpdateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.convert.contract.ContractConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contract.ContractMapper;
@@ -19,6 +16,8 @@ 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.ErrorCodeConstants.CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED;
+import static cn.iocoder.yudao.module.crm.framework.utils.AuthUtil.isReadAndWrite;
 
 /**
  * 合同 Service 实现类
@@ -58,10 +57,12 @@ public class ContractServiceImpl implements ContractService {
         contractMapper.deleteById(id);
     }
 
-    private void validateContractExists(Long id) {
-        if (contractMapper.selectById(id) == null) {
+    private ContractDO validateContractExists(Long id) {
+        ContractDO contract = contractMapper.selectById(id);
+        if (contract == null) {
             throw exception(CONTRACT_NOT_EXISTS);
         }
+        return contract;
     }
 
     @Override
@@ -87,4 +88,21 @@ public class ContractServiceImpl implements ContractService {
         return contractMapper.selectList(exportReqVO);
     }
 
+    @Override
+    public void contractTransfer(CrmContractTransferReqVO reqVO, Long userId) {
+        // 1. 校验合同是否存在
+        ContractDO contract = validateContractExists(reqVO.getId());
+        // 1.2. 校验用户是否拥有读写权限
+        if (!isReadAndWrite(contract.getRwUserIds(), userId)) {
+            throw exception(CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED);
+        }
+
+        // 2. 更新新的负责人
+        ContractDO updateContract = ContractConvert.INSTANCE.convert(contract, reqVO, userId);
+        contractMapper.updateById(updateContract);
+
+        // 3. TODO 记录合同转移日志
+
+    }
+
 }

From 36a76c5ce63e8988e3c086cdb90d1a3080b0e254 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Sun, 29 Oct 2023 01:42:22 +0800
Subject: [PATCH 031/101] =?UTF-8?q?CRM-=E8=81=94=E7=B3=BB=E4=BA=BA?=
 =?UTF-8?q?=EF=BC=9A=E6=96=B0=E5=A2=9E=E8=81=94=E7=B3=BB=E4=BA=BA=E8=BD=AC?=
 =?UTF-8?q?=E7=A7=BB=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |  3 ++
 .../admin/contact/ContactController.java      | 48 +++++++++++--------
 .../admin/contact/vo/ContactCreateReqVO.java  | 14 ++++--
 .../admin/contact/vo/ContactUpdateReqVO.java  | 14 ++++--
 .../contact/vo/CrmContactTransferReqVO.java   | 20 ++++++++
 .../crm/convert/contact/ContactConvert.java   | 19 ++++++--
 .../crm/dal/dataobject/contact/ContactDO.java | 14 ++++++
 .../crm/service/contact/ContactService.java   | 16 +++++--
 .../service/contact/ContactServiceImpl.java   | 41 +++++++++++++---
 .../service/contract/ContractServiceImpl.java | 17 +++++--
 10 files changed, 160 insertions(+), 46 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index dcb03a74d..f5b5e078b 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -12,6 +12,7 @@ public interface ErrorCodeConstants {
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
     ErrorCode CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_000_001, "合同转移失败,原因:没有转移权限");
+    ErrorCode CONTRACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_000_002, "合同转移失败,原因:负责人不存在");
 
     // ========== 线索管理 1-020-001-000 ==========
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
@@ -26,6 +27,8 @@ public interface ErrorCodeConstants {
 
     // ========== 联系人管理 1-020-003-000 ==========
     ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
+    ErrorCode CONTACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_003_001, "联系人转移失败,原因:没有转移权限");
+    ErrorCode CONTACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_003_002, "联系人转移失败,原因:负责人不存在");
 
     // TODO @liuhongfeng:错误码分段;
     ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_030_000_001, "回款管理不存在");
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
index bafec03c1..f28d27ef8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -1,32 +1,30 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact;
 
-import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.security.access.prepost.PreAuthorize;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.Operation;
-
-import javax.validation.constraints.*;
-import javax.validation.*;
-import javax.servlet.http.*;
-import java.util.*;
-import java.io.IOException;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
-
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.module.crm.service.contact.ContactService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+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;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - crm联系人")
 @RestController
@@ -99,4 +97,12 @@ public class ContactController {
         ExcelUtils.write(response, "crm联系人.xls", "数据", ContactExcelVO.class, datas);
     }
 
+    @PutMapping("/transfer")
+    @Operation(summary = "联系人转移")
+    @PreAuthorize("@ss.hasPermission('crm:contact:update')")
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContactTransferReqVO reqVO) {
+        contactService.contactTransfer(reqVO, getLoginUserId());
+        return success(true);
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
index 424d945dc..64ea71084 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
-import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.Set;
 
 @Schema(description = "管理后台 - crm联系人创建 Request VO")
 @Data
@@ -11,4 +13,10 @@ import javax.validation.constraints.*;
 @ToString(callSuper = true)
 public class ContactCreateReqVO extends ContactBaseVO {
 
+    @Schema(description = "只读权限的用户编号数组")
+    private Set<Long> roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private Set<Long> rwUserIds;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
index 19db67297..60afd06f2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.Set;
 
 @Schema(description = "管理后台 - crm联系人更新 Request VO")
 @Data
@@ -14,4 +16,10 @@ public class ContactUpdateReqVO extends ContactBaseVO {
     @Schema(description = "主键", example = "23210")
     private Long id;
 
+    @Schema(description = "只读权限的用户编号数组")
+    private Set<Long> roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private Set<Long> rwUserIds;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
new file mode 100644
index 000000000..517d77576
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 联系转移 Request VO")
+@Data
+public class CrmContactTransferReqVO {
+
+    @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long ownerUserId; // 新的负责人
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index 3892806c0..2718b339a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -1,13 +1,14 @@
 package cn.iocoder.yudao.module.crm.convert.contact;
 
-import java.util.*;
-
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+import java.util.Set;
 
 /**
  * crm联系人 Convert
@@ -31,4 +32,12 @@ public interface ContactConvert {
 
     List<ContactExcelVO> convertList02(List<ContactDO> list);
 
+    default ContactDO convert(ContactDO contact, CrmContactTransferReqVO reqVO, Long userId) {
+        Set<Long> rwUserIds = contact.getRwUserIds();
+        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId.toString())); // 移除老负责人
+        rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
+        return new ContactDO().setId(contact.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
+                .setRwUserIds(rwUserIds);
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
index 2e7d2cfef..6c56ebe98 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
@@ -1,12 +1,15 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
 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.*;
 
 import java.time.LocalDateTime;
+import java.util.Set;
 
 /**
  * crm 联系人 DO
@@ -77,4 +80,15 @@ public class ContactDO extends BaseDO {
      */
     private LocalDateTime lastTime;
 
+    /**
+     * 只读权限的用户编号数组
+     */
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> rwUserIds;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
index 97a221ee4..63b3449f2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.crm.service.contact;
 
-import java.util.*;
-import javax.validation.*;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
 
 /**
  * crm联系人 Service 接口
@@ -67,4 +69,12 @@ public interface ContactService {
      */
     List<ContactDO> getContactList(ContactExportReqVO exportReqVO);
 
+    /**
+     * 联系人编号
+     *
+     * @param reqVO  请求
+     * @param userId 用户编号
+     */
+    void contactTransfer(CrmContactTransferReqVO reqVO, Long userId);
+    
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index b21fee020..eb2b97cbe 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -3,13 +3,12 @@ package cn.iocoder.yudao.module.crm.service.contact;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactUpdateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -18,7 +17,8 @@ import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTACT_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.crm.framework.utils.AuthUtil.isReadAndWrite;
 
 /**
  * crm联系人 Service 实现类
@@ -32,6 +32,9 @@ public class ContactServiceImpl implements ContactService {
     @Resource
     private ContactMapper contactMapper;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @Override
     public Long createContact(ContactCreateReqVO createReqVO) {
         // TODO @customerId:需要校验存在
@@ -61,10 +64,12 @@ public class ContactServiceImpl implements ContactService {
         contactMapper.deleteById(id);
     }
 
-    private void validateContactExists(Long id) {
-        if (contactMapper.selectById(id) == null) {
+    private ContactDO validateContactExists(Long id) {
+        ContactDO contact = contactMapper.selectById(id);
+        if (contact == null) {
             throw exception(CONTACT_NOT_EXISTS);
         }
+        return contact;
     }
 
     @Override
@@ -90,4 +95,26 @@ public class ContactServiceImpl implements ContactService {
         return contactMapper.selectList(exportReqVO);
     }
 
+    @Override
+    public void contactTransfer(CrmContactTransferReqVO reqVO, Long userId) {
+        // 1. 校验联系人是否存在
+        ContactDO contact = validateContactExists(reqVO.getId());
+        // 1.2. 校验用户是否拥有读写权限
+        if (!isReadAndWrite(contact.getRwUserIds(), userId)) {
+            throw exception(CONTACT_TRANSFER_FAIL_PERMISSION_DENIED);
+        }
+        // 2. 校验新负责人是否存在
+        AdminUserRespDTO user = adminUserApi.getUser(reqVO.getOwnerUserId());
+        if (user == null) {
+            throw exception(CONTACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS);
+        }
+
+        // 3. 更新新的负责人
+        ContactDO updateContract = ContactConvert.INSTANCE.convert(contact, reqVO, userId);
+        contactMapper.updateById(updateContract);
+
+        // 4. TODO 记录联系人转移日志
+
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index 4dadd6923..8cee161f0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.convert.contract.ContractConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contract.ContractMapper;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -15,8 +17,7 @@ import java.util.Collection;
 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.ErrorCodeConstants.CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.crm.framework.utils.AuthUtil.isReadAndWrite;
 
 /**
@@ -31,6 +32,9 @@ public class ContractServiceImpl implements ContractService {
     @Resource
     private ContractMapper contractMapper;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @Override
     public Long createContract(ContractCreateReqVO createReqVO) {
         // 插入
@@ -96,12 +100,17 @@ public class ContractServiceImpl implements ContractService {
         if (!isReadAndWrite(contract.getRwUserIds(), userId)) {
             throw exception(CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED);
         }
+        // 2. 校验新负责人是否存在
+        AdminUserRespDTO user = adminUserApi.getUser(reqVO.getOwnerUserId());
+        if (user == null) {
+            throw exception(CONTRACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS);
+        }
 
-        // 2. 更新新的负责人
+        // 3. 更新新的负责人
         ContractDO updateContract = ContractConvert.INSTANCE.convert(contract, reqVO, userId);
         contractMapper.updateById(updateContract);
 
-        // 3. TODO 记录合同转移日志
+        // 4. TODO 记录合同转移日志
 
     }
 

From 722e9c8a044c9deea0cdf4e3f316bb5fbcc91c7f Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Sun, 29 Oct 2023 02:04:26 +0800
Subject: [PATCH 032/101] =?UTF-8?q?CRM-=E5=95=86=E6=9C=BA=EF=BC=9A?=
 =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=95=86=E6=9C=BA=E8=BD=AC=E7=A7=BB=E6=8E=A5?=
 =?UTF-8?q?=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |  2 +
 .../admin/business/CrmBusinessController.java | 11 ++++-
 .../business/vo/CrmBusinessCreateReqVO.java   |  9 ++++
 .../business/vo/CrmBusinessTransferReqVO.java | 20 +++++++++
 .../business/vo/CrmBusinessUpdateReqVO.java   |  8 ++++
 .../contact/vo/CrmContactTransferReqVO.java   |  2 +-
 .../convert/business/CrmBusinessConvert.java  | 19 ++++++---
 .../crm/convert/contact/ContactConvert.java   |  2 +-
 .../crm/convert/contract/ContractConvert.java |  2 +-
 .../dataobject/business/CrmBusinessDO.java    | 13 +++---
 .../service/business/CrmBusinessService.java  | 13 ++++--
 .../business/CrmBusinessServiceImpl.java      | 41 +++++++++++++++----
 .../service/contact/ContactServiceImpl.java   |  4 +-
 13 files changed, 118 insertions(+), 28 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index f5b5e078b..7c1f351d8 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -19,6 +19,8 @@ public interface ErrorCodeConstants {
 
     // ========== 商机管理 1-020-002-000 ==========
     ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
+    ErrorCode BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_002_001, "商机转移失败,原因:没有转移权限");
+    ErrorCode BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_002_002, "商机转移失败,原因:负责人不存在");
 
     // TODO @lilleo:商机状态、商机类型,都单独错误码段
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index d517a9f91..8096176c9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -23,6 +23,7 @@ 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;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 商机")
 @RestController
@@ -80,11 +81,19 @@ public class CrmBusinessController {
     @PreAuthorize("@ss.hasPermission('crm:business:export')")
     @OperateLog(type = EXPORT)
     public void exportBusinessExcel(@Valid CrmBusinessExportReqVO exportReqVO,
-              HttpServletResponse response) throws IOException {
+                                    HttpServletResponse response) throws IOException {
         List<CrmBusinessDO> list = businessService.getBusinessList(exportReqVO);
         // 导出 Excel
         List<CrmBusinessExcelVO> datas = CrmBusinessConvert.INSTANCE.convertList02(list);
         ExcelUtils.write(response, "商机.xls", "数据", CrmBusinessExcelVO.class, datas);
     }
 
+    @PutMapping("/transfer")
+    @Operation(summary = "商机转移")
+    @PreAuthorize("@ss.hasPermission('crm:business:update')")
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmBusinessTransferReqVO reqVO) {
+        businessService.businessTransfer(reqVO, getLoginUserId());
+        return success(true);
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
index f743c8469..968a105c8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
@@ -5,12 +5,21 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
+import java.util.Set;
+
 @Schema(description = "管理后台 - 商机创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class CrmBusinessCreateReqVO extends CrmBusinessBaseVO {
 
+    @Schema(description = "只读权限的用户编号数组")
+    private Set<Long> roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private Set<Long> rwUserIds;
+
+
     // TODO @ljileo:新建的时候,应该可以传递添加的产品;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
new file mode 100644
index 000000000..993e4750e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 商机转移 Request VO")
+@Data
+public class CrmBusinessTransferReqVO {
+
+    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long ownerUserId; // 新的负责人
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
index f137d4c5b..2929535cf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
@@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 import javax.validation.constraints.NotNull;
+import java.util.Set;
 
 @Schema(description = "管理后台 - 商机更新 Request VO")
 @Data
@@ -17,6 +18,13 @@ public class CrmBusinessUpdateReqVO extends CrmBusinessBaseVO {
     @NotNull(message = "主键不能为空")
     private Long id;
 
+    @Schema(description = "只读权限的用户编号数组")
+    private Set<Long> roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private Set<Long> rwUserIds;
+
+
     // TODO @ljileo:修改的时候,应该可以传递添加的产品;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
index 517d77576..90acc5645 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
@@ -5,7 +5,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 
-@Schema(description = "管理后台 - 联系转移 Request VO")
+@Schema(description = "管理后台 - 联系人转移 Request VO")
 @Data
 public class CrmContactTransferReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index 72aad68de..2eabf7d4d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -1,13 +1,14 @@
 package cn.iocoder.yudao.module.crm.convert.business;
 
-import java.util.*;
-
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+import java.util.Set;
 
 /**
  * 商机 Convert
@@ -29,4 +30,12 @@ public interface CrmBusinessConvert {
 
     List<CrmBusinessExcelVO> convertList02(List<CrmBusinessDO> list);
 
+    default CrmBusinessDO convert(CrmBusinessDO business, CrmBusinessTransferReqVO reqVO, Long userId) {
+        Set<Long> rwUserIds = business.getRwUserIds();
+        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
+        rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
+        return new CrmBusinessDO().setId(business.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
+                .setRwUserIds(rwUserIds);
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index 2718b339a..0d1329819 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -34,7 +34,7 @@ public interface ContactConvert {
 
     default ContactDO convert(ContactDO contact, CrmContactTransferReqVO reqVO, Long userId) {
         Set<Long> rwUserIds = contact.getRwUserIds();
-        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId.toString())); // 移除老负责人
+        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
         rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
         return new ContactDO().setId(contact.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
                 .setRwUserIds(rwUserIds);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index 023d4200b..d7547d4b8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -34,7 +34,7 @@ public interface ContractConvert {
 
     default ContractDO convert(ContractDO contract, CrmContractTransferReqVO reqVO, Long userId) {
         Set<Long> rwUserIds = contract.getRwUserIds();
-        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId.toString())); // 移除老负责人
+        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
         rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
         return new ContractDO().setId(contract.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
                 .setRwUserIds(rwUserIds);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
index 2d6454838..23d7ddb9a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
@@ -1,14 +1,17 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.business;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
 import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
 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.*;
 
 import java.time.LocalDateTime;
+import java.util.Set;
 
 /**
  * 商机 DO
@@ -85,16 +88,14 @@ public class CrmBusinessDO extends BaseDO {
     private Long ownerUserId;
     /**
      * 只读权限的用户编号数组
-     *
-     * TODO @lijie:应该是 List<Long>,然后使用下对应的 typehandler
      */
-    private String roUserIds;
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> roUserIds;
     /**
      * 读写权限的用户编号数组
-     *
-     * TODO @lijie:应该是 List<Long>,然后使用下对应的 typehandler
      */
-    private String rwUserIds;
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> rwUserIds;
     /**
      * 1赢单2输单3无效
      *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index 4bb352c58..8a157440b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -1,10 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.business;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessUpdateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 
 import javax.validation.Valid;
@@ -72,4 +69,12 @@ public interface CrmBusinessService {
      */
     List<CrmBusinessDO> getBusinessList(CrmBusinessExportReqVO exportReqVO);
 
+    /**
+     * 商机转移
+     *
+     * @param reqVO  请求
+     * @param userId 用户编号
+     */
+    void businessTransfer(CrmBusinessTransferReqVO reqVO, Long userId);
+    
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index 44fa74f80..4ddb3363e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -3,13 +3,12 @@ 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.module.crm.controller.admin.business.vo.CrmBusinessCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessUpdateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -18,7 +17,8 @@ import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.crm.framework.utils.AuthUtil.isReadAndWrite;
 
 /**
  * 商机 Service 实现类
@@ -32,6 +32,9 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     @Resource
     private CrmBusinessMapper businessMapper;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @Override
     public Long createBusiness(CrmBusinessCreateReqVO createReqVO) {
         // 插入
@@ -58,10 +61,12 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         businessMapper.deleteById(id);
     }
 
-    private void validateBusinessExists(Long id) {
-        if (businessMapper.selectById(id) == null) {
+    private CrmBusinessDO validateBusinessExists(Long id) {
+        CrmBusinessDO crmBusiness = businessMapper.selectById(id);
+        if (crmBusiness == null) {
             throw exception(BUSINESS_NOT_EXISTS);
         }
+        return crmBusiness;
     }
 
     @Override
@@ -87,4 +92,26 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         return businessMapper.selectList(exportReqVO);
     }
 
+    @Override
+    public void businessTransfer(CrmBusinessTransferReqVO reqVO, Long userId) {
+        // 1. 校验商机是否存在
+        CrmBusinessDO business = validateBusinessExists(reqVO.getId());
+        // 1.2. 校验用户是否拥有读写权限
+        if (!isReadAndWrite(business.getRwUserIds(), userId)) {
+            throw exception(BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED);
+        }
+        // 2. 校验新负责人是否存在
+        AdminUserRespDTO user = adminUserApi.getUser(reqVO.getOwnerUserId());
+        if (user == null) {
+            throw exception(BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS);
+        }
+
+        // 3. 更新新的负责人
+        CrmBusinessDO updateBusiness = CrmBusinessConvert.INSTANCE.convert(business, reqVO, userId);
+        businessMapper.updateById(updateBusiness);
+
+        // 4. TODO 记录商机转移日志
+
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index eb2b97cbe..e47e2e155 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -110,8 +110,8 @@ public class ContactServiceImpl implements ContactService {
         }
 
         // 3. 更新新的负责人
-        ContactDO updateContract = ContactConvert.INSTANCE.convert(contact, reqVO, userId);
-        contactMapper.updateById(updateContract);
+        ContactDO updateContact = ContactConvert.INSTANCE.convert(contact, reqVO, userId);
+        contactMapper.updateById(updateContact);
 
         // 4. TODO 记录联系人转移日志
 

From fe50356ae8cedc4ffacdcc87aca00e56535b4e4d Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 29 Oct 2023 17:38:59 +0800
Subject: [PATCH 033/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E6=A8=A1=E5=9D=97=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             | 40 +----------
 sql/mysql/crm_data.sql                        | 38 -----------
 sql/mysql/crm_menu.sql                        | 67 -------------------
 .../module/crm/enums/DictTypeConstants.java   |  2 +-
 .../enums/customer/CrmCustomerLevelEnum.java  |  7 +-
 .../admin/customer/vo/CrmCustomerBaseVO.java  |  9 ++-
 .../customer/vo/CrmCustomerCreateReqVO.java   |  2 +
 .../dataobject/customer/CrmCustomerDO.java    |  9 ++-
 .../dal/mysql/customer/CrmCustomerMapper.java |  2 +-
 .../service/customer/CrmCustomerService.java  |  4 +-
 .../customer/CrmCustomerServiceImpl.java      | 44 +++++++-----
 11 files changed, 48 insertions(+), 176 deletions(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 51b57412a..677ea2881 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -185,45 +185,7 @@ CREATE TABLE `crm_contact` (
                                PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='crm联系人';
 
--- ----------------------------
--- 客户表
--- ----------------------------
-DROP TABLE IF EXISTS `crm_customer`;
-CREATE TABLE `crm_customer`  (
- `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
- `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户名称',
- `follow_up_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '跟进状态',
- `lock_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '锁定状态',
- `deal_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '成交状态',
- `industry_id` int NULL DEFAULT NULL COMMENT '所属行业',
- `level` int NULL DEFAULT NULL COMMENT '客户等级',
- `source` int NULL DEFAULT NULL COMMENT '客户来源',
- `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机',
- `telephone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电话',
- `website` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '网址',
- `qq` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'QQ',
- `wechat` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信',
- `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
- `description` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户描述',
- `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
- `owner_user_id` bigint NULL DEFAULT NULL COMMENT '负责人的用户编号',
- `ro_user_ids` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '只读权限的用户编号数组',
- `rw_user_ids` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '读写权限的用户编号数组',
- `area_id` bigint NULL DEFAULT NULL COMMENT '地区编号',
- `detail_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '详细地址',
- `longitude` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地理位置经度',
- `latitude` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地理位置维度',
- `contact_last_time` datetime NULL DEFAULT NULL COMMENT '最后跟进时间',
- `contact_next_time` datetime NULL DEFAULT NULL COMMENT '下次联系时间',
- `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
- `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
- `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
- `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
- PRIMARY KEY (`id`) USING BTREE,
- INDEX `owner_user_id`(`owner_user_id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客户表' ROW_FORMAT = Dynamic;
+
 -- ----------------------------
 -- 商机状态表
 -- ----------------------------
diff --git a/sql/mysql/crm_data.sql b/sql/mysql/crm_data.sql
index 63b7d9f72..2d742698d 100644
--- a/sql/mysql/crm_data.sql
+++ b/sql/mysql/crm_data.sql
@@ -18,41 +18,3 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1401, 8, '其他', '8', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:56:06', '1', '2023-10-18 21:56:06', b'0');
 
 
-
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (188, '客户来源', 'crm_customer_source', 0, 'CRM 客户来源', '1', '2023-10-28 23:00:34', '1', '2023-10-28 15:11:16', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (187, '客户等级', 'crm_customer_level', 0, 'CRM 客户等级', '1', '2023-10-28 22:59:12', '1', '2023-10-28 15:11:16', b'0', NULL);
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (186, '客户所属行业', 'crm_customer_industry', 0, 'CRM 客户所属行业', '1', '2023-10-28 22:57:07', '1', '2023-10-28 15:11:16', b'0', NULL);
-
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1434, 10, '邮件咨询', '10', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:33', '1', '2023-10-28 23:10:33', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1433, 9, '电话咨询', '9', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:18', '1', '2023-10-28 23:10:18', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1432, 8, '陌拜', '8', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:04', '1', '2023-10-28 23:10:04', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1431, 7, '预约上门', '7', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:39', '1', '2023-10-28 23:09:39', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1430, 6, '线上咨询', '6', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:22', '1', '2023-10-28 23:09:22', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1429, 5, '线上注册', '5', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:12', '1', '2023-10-28 23:09:12', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1428, 4, '转介绍', '4', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:58', '1', '2023-10-28 23:08:58', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1427, 3, '广告', '3', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:47', '1', '2023-10-28 23:08:47', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1426, 2, '搜索引擎', '2', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:39', '1', '2023-10-28 23:08:39', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1425, 1, '促销', '1', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:29', '1', '2023-10-28 23:08:29', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1424, 3, 'C (非优先客户)', '3', 'crm_customer_level', 0, 'default', '', '', '1', '2023-10-28 23:07:53', '1', '2023-10-28 23:07:53', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1423, 2, 'B (普通客户)', '2', 'crm_customer_level', 0, 'info', '', '', '1', '2023-10-28 23:07:35', '1', '2023-10-28 23:07:35', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1422, 1, 'A (重点客户)', '1', 'crm_customer_level', 0, 'primary', '', '', '1', '2023-10-28 23:07:13', '1', '2023-10-28 23:07:13', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1421, 20, 'T 国际组织', '20', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:06:15', '1', '2023-10-28 23:06:15', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1420, 19, 'S 公共管理、社会保障和社会组织', '19', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:06:05', '1', '2023-10-28 23:06:05', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1419, 18, 'R 文化、体育和娱乐业', '18', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:55', '1', '2023-10-28 23:05:55', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1418, 17, 'Q 卫生和社会工作', '17', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:44', '1', '2023-10-28 23:05:44', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1417, 16, 'P 教育', '16', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:15', '1', '2023-10-28 23:05:15', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1416, 15, 'O 居民服务、修理和其他服务业', '15', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:05', '1', '2023-10-28 23:05:05', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1415, 14, 'N 水利、环境和公共设施管理业', '14', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:53', '1', '2023-10-28 23:04:53', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1414, 13, 'M 科学研究和技术服务业', '13', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:43', '1', '2023-10-28 23:04:43', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1413, 12, 'L 租赁和商务服务业', '12', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:33', '1', '2023-10-28 23:04:33', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1412, 11, 'K 房地产业', '11', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:15', '1', '2023-10-28 23:04:22', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1411, 10, 'J 金融业', '10', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:57', '1', '2023-10-28 23:03:57', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1410, 9, 'I 信息传输、软件和信息技术服务业', '9', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:47', '1', '2023-10-28 23:03:47', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1409, 8, 'H 住宿和餐饮业', '8', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:37', '1', '2023-10-28 23:03:37', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1408, 7, 'G 交通运输、仓储和邮政业', '7', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:27', '1', '2023-10-28 23:03:27', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1407, 6, 'F 批发和零售业', '6', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:13', '1', '2023-10-28 23:03:13', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1406, 5, 'E 建筑业', '5', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:03', '1', '2023-10-28 23:03:03', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1405, 4, 'D 电力、热力、燃气及水生产和供应业', '4', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:54', '1', '2023-10-28 23:02:54', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1404, 3, 'C 制造业', '3', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:41', '1', '2023-10-28 23:02:41', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1403, 2, 'B 采矿业', '2', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:29', '1', '2023-10-28 23:02:29', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1402, 1, 'A 农、林、牧、渔业', '1', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:15', '1', '2023-10-28 23:02:15', b'0');
\ No newline at end of file
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index f75e98f08..d0c9b6dce 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -233,13 +233,6 @@ VALUES (
            '', '', '', 0
        );
 
-
--- ----------------------------
--- 客户管理菜单
--- ----------------------------
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2375, '客户管理', '', 1, 0, 0, '/crm', 'ep:avatar', '', '', 0, b'1', b'1', b'1', '1', '2023-10-20 00:36:13', '1', '2023-10-19 16:37:24', b'0');
-
-
 -- ----------------------------
 -- 回款菜单
 -- ----------------------------
@@ -360,63 +353,3 @@ VALUES (
            '回款计划导出', 'crm:receivable-plan:export', 3, 5, @parentId,
            '', '', '', 0
        );
-
--- ----------------------------
--- 客户管理菜单
--- ----------------------------
-
--- 菜单 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status, component_name
-)
-VALUES (
-   '客户管理', '', 2, 0, 2375,
-   'customer', '', 'crm/customer/index', 0, 'CrmCustomer'
-);
-
--- 按钮父菜单ID
--- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
-SELECT @parentId := LAST_INSERT_ID();
-
--- 按钮 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '客户查询', 'crm:customer:query', 3, 1, @parentId,
-   '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '客户创建', 'crm:customer:create', 3, 2, @parentId,
-   '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '客户更新', 'crm:customer:update', 3, 3, @parentId,
-   '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '客户删除', 'crm:customer:delete', 3, 4, @parentId,
-   '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '客户导出', 'crm:customer:export', 3, 5, @parentId,
-   '', '', '', 0
-);
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
index dd2c068d3..0e412ee9b 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.crm.enums;
 
 /**
- * System 字典类型的枚举类
+ * CRM 字典类型的枚举类
  *
  * @author 芋道源码
  */
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
index c2da3744e..f58028314 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
@@ -15,12 +15,13 @@ import java.util.Arrays;
 @AllArgsConstructor
 public enum CrmCustomerLevelEnum implements IntArrayValuable {
 
-    IMPORTANT(1, "A (重点客户)"),
-    GENERAL(2, "B (普通客户)"),
-    LOW_PRIORITY(3, "C (非优先客户)");
+    IMPORTANT(1, "A(重点客户)"),
+    GENERAL(2, "B(普通客户)"),
+    LOW_PRIORITY(3, "C(非优先客户)");
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLevelEnum::getStatus).toArray();
 
+    // TODO @wanwan:这里的 status 字段,可以考虑改成 level
     /**
      * 状态
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
index f3b81151c..0e4d4463d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
@@ -10,7 +10,6 @@ import org.springframework.format.annotation.DateTimeFormat;
 
 import javax.validation.constraints.Email;
 import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
 import javax.validation.constraints.Size;
 import java.time.LocalDateTime;
 
@@ -49,20 +48,20 @@ public class CrmCustomerBaseVO {
     private String website;
 
     @Schema(description = "QQ", example = "123456789")
-    @Size(max = 20, message = "QQ长度不能超过20个字符")
+    @Size(max = 20, message = "QQ长度不能超过 20 个字符")
     private String qq;
 
     @Schema(description = "wechat", example = "123456789")
-    @Size(max = 255, message = "微信长度不能超过255个字符")
+    @Size(max = 255, message = "微信长度不能超过 255 个字符")
     private String wechat;
 
     @Schema(description = "email", example = "123456789@qq.com")
     @Email(message = "邮箱格式不正确")
-    @Size(max = 255, message = "邮箱长度不能超过255个字符")
+    @Size(max = 255, message = "邮箱长度不能超过 255 个字符")
     private String email;
 
     @Schema(description = "客户描述", example = "任意文字")
-    @Size(max = 4096, message = "客户描述长度不能超过255个字符")
+    @Size(max = 4096, message = "客户描述长度不能超过 4096 个字符")
     private String description;
 
     @Schema(description = "备注", example = "随便")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
index ab806b689..84462ddfa 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
@@ -11,4 +11,6 @@ import lombok.ToString;
 @ToString(callSuper = true)
 public class CrmCustomerCreateReqVO extends CrmCustomerBaseVO {
 
+    // TODO @wanwan:负责人
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index a50ada4c5..6e5026c94 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -51,17 +51,20 @@ public class CrmCustomerDO extends BaseDO {
     private Boolean dealStatus;
     /**
      * 所属行业
-     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY}
+     *
+     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_CUSTOMER_INDUSTRY}
      */
     private Integer industryId;
     /**
      * 客户等级
-     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL}
+     *
+     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_CUSTOMER_LEVEL}
      */
     private Integer level;
     /**
      * 客户来源
-     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE}
+     *
+     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_CUSTOMER_SOURCE}
      */
     private Integer source;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
index 838423086..0b2497a7b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -19,7 +19,7 @@ import java.util.List;
 public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
 
     default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO reqVO) {
-        // TODO @Wanwan 填充负责人,所属部门字段
+        // TODO @Wanwan 填充负责人,所属部门字段;这个可以在 controller 填哈;
         return selectPage(reqVO, new LambdaQueryWrapperX<CrmCustomerDO>()
                 .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
                 .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index 4e41fab25..73c6af2b6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -75,8 +75,8 @@ public interface CrmCustomerService {
     /**
      * 校验客户是否存在
      *
-     * @param customerId 客户id
-     * @return
+     * @param customerId 客户 id
+     * @return 客户
      */
     CrmCustomerDO validateCustomer(Long customerId);
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index fb27d4ae9..15535f681 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -1,23 +1,26 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
-import cn.iocoder.yudao.module.system.api.dept.DeptApi;
-import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-
-import java.util.*;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
-import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
 
 /**
  * 客户 Service 实现类
@@ -31,7 +34,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     @Resource
     private CrmCustomerMapper customerMapper;
     @Resource
-    private DeptApi deptApi;
+    private DeptApi deptApi; // TODO @wanwan:拼接数据,可以放到 controller;所以这里的引入,可以考虑放到 controller 哈;
 
     @Override
     public Long createCustomer(CrmCustomerCreateReqVO createReqVO) {
@@ -46,6 +49,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     public void updateCustomer(CrmCustomerUpdateReqVO updateReqVO) {
         // 校验存在
         validateCustomerExists(updateReqVO.getId());
+        // TODO 芋艿:数据权限,校验是否可以操作
+
         // 更新
         CrmCustomerDO updateObj = CrmCustomerConvert.INSTANCE.convert(updateReqVO);
         customerMapper.updateById(updateObj);
@@ -55,6 +60,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     public void deleteCustomer(Long id) {
         // 校验存在
         validateCustomerExists(id);
+        // TODO 芋艿:数据权限,校验是否可以操作
+
         // 删除
         customerMapper.deleteById(id);
     }
@@ -80,6 +87,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     public PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO) {
+        // TODO 芋艿:数据权限,是否可以查询到;
         return customerMapper.selectPage(pageReqVO);
     }
 
@@ -88,6 +96,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         return customerMapper.selectList(exportReqVO);
     }
 
+    // TODO wanwan:service 接口已经注释,实现类就不需要了。
     /**
      * 校验客户是否存在
      *
@@ -102,4 +111,5 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         }
         return customer;
     }
+
 }

From 677e8ab2fc0c50185425ba24b90d0049099ab027 Mon Sep 17 00:00:00 2001
From: liuhongfeng <291117974@qq.com>
Date: Sun, 29 Oct 2023 18:00:34 +0800
Subject: [PATCH 034/101] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E3=80=91?=
 =?UTF-8?q?=E5=9B=9E=E6=AC=BE=E8=AE=A1=E5=88=92-=E4=BF=AE=E6=94=B9?=
 =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=B1=BB=E5=9E=8B=20=E3=80=90=E4=BF=AE?=
 =?UTF-8?q?=E6=94=B9=E3=80=91=E4=BF=AE=E6=94=B9=E5=9B=9E=E6=AC=BE=E8=AE=A1?=
 =?UTF-8?q?=E5=88=92-=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C?=
 =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=A2=E6=88=B7=E7=9A=84=E6=95=B0=E6=8D=AE?=
 =?UTF-8?q?=E6=A0=A1=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/AuditStatusEnum.java     |  2 +-
 .../receivable/vo/ReceivablePlanBaseVO.java   |  2 +-
 .../receivable/vo/ReceivablePlanExcelVO.java  |  2 +-
 .../vo/ReceivablePlanExportReqVO.java         |  2 +-
 .../vo/ReceivablePlanPageReqVO.java           |  2 +-
 .../receivable/ReceivablePlanDO.java          |  2 +-
 .../receivable/ReceivablePlanServiceImpl.java | 39 +++++++++++++++++--
 .../receivable/ReceivableServiceImpl.java     | 21 ++++++++--
 8 files changed, 59 insertions(+), 13 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
index ce04c18a1..6cc1012b6 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
@@ -17,7 +17,7 @@ public enum AuditStatusEnum implements IntArrayValuable {
     /**
      * 审核通过
      */
-	AUDIT_FINISH(0, "审核通过"),
+	AUDIT_FINISH(1, "审核通过"),
     /**
      * 审核拒绝
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
index 59f333e4b..53572e2c4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
@@ -31,7 +31,7 @@ public class ReceivablePlanBaseVO {
     // TODO @liuhongfeng:这个字段,可以写个枚举,然后 InEnum 去校验下;
     // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的
     @Schema(description = "审批状态", example = "1")
-    private String checkStatus;
+    private Integer checkStatus;
 
     // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
     @Schema(description = "工作流编号", example = "8909")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
index e12d5a1df..d08cfc6a7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
@@ -37,7 +37,7 @@ public class ReceivablePlanExcelVO {
 
     @ExcelProperty(value = "审批状态", converter = DictConvert.class)
     @DictFormat("crm_receivable_check_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
-    private String checkStatus;
+    private Integer checkStatus;
 
     //@ExcelProperty("工作流编号")
     //private Long processInstanceId;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
index ca5d3f553..e68427d29 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
@@ -20,7 +20,7 @@ public class ReceivablePlanExportReqVO {
     private Integer status;
 
     @Schema(description = "审批状态", example = "1")
-    private String checkStatus;
+    private Integer checkStatus;
 
     @Schema(description = "计划回款日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
index 2d0e7d5ae..acd4d5aa4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
@@ -26,7 +26,7 @@ public class ReceivablePlanPageReqVO extends PageParam {
     private Integer status;
 
     @Schema(description = "审批状态", example = "1")
-    private String checkStatus;
+    private Integer checkStatus;
 
     @Schema(description = "计划回款日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
index 4447df053..0c6f6e033 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
@@ -49,7 +49,7 @@ public class ReceivablePlanDO extends BaseDO {
      *
      * 枚举 {@link TODO crm_receivable_check_status 对应的类}
      */
-    private String checkStatus;
+    private Integer checkStatus;
     /**
      * 工作流编号
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
index c0b236ac0..f8493a1ec 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
@@ -10,8 +10,14 @@ import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlan
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.receivable.ReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper;
+import cn.iocoder.yudao.module.crm.enums.AuditStatusEnum;
+import cn.iocoder.yudao.module.crm.service.contract.ContractService;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -20,7 +26,7 @@ import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.RECEIVABLE_PLAN_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 
 /**
  * 回款计划 Service 实现类
@@ -33,20 +39,47 @@ public class ReceivablePlanServiceImpl implements ReceivablePlanService {
 
     @Resource
     private ReceivablePlanMapper receivablePlanMapper;
+    @Resource
+    private ContractService contractService;
+    @Resource
+    private CrmCustomerService crmCustomerService;
 
     @Override
     public Long createReceivablePlan(ReceivablePlanCreateReqVO createReqVO) {
         // 插入
         ReceivablePlanDO receivablePlan = ReceivablePlanConvert.INSTANCE.convert(createReqVO);
-        // TODO @liuhongfeng:空格要注释;if (ObjectUtil.isNull(receivablePlan.getStatus())) {
-        if(ObjectUtil.isNull(receivablePlan.getStatus())){
+        if (ObjectUtil.isNull(receivablePlan.getStatus())){
             receivablePlan.setStatus(CommonStatusEnum.ENABLE.getStatus());
         }
+        if (ObjectUtil.isNull(receivablePlan.getCheckStatus())){
+            receivablePlan.setCheckStatus(AuditStatusEnum.AUDIT_NEW.getValue());
+        }
+
+        checkReceivablePlan(receivablePlan);
+
         receivablePlanMapper.insert(receivablePlan);
         // 返回
         return receivablePlan.getId();
     }
 
+    private void checkReceivablePlan(ReceivablePlanDO receivablePlan) {
+
+        if(ObjectUtil.isNull(receivablePlan.getContractId())){
+            throw exception(CONTRACT_NOT_EXISTS);
+        }
+
+        ContractDO contract = contractService.getContract(receivablePlan.getContractId());
+        if(ObjectUtil.isNull(contract)){
+            throw exception(CONTRACT_NOT_EXISTS);
+        }
+
+        CrmCustomerDO customer = crmCustomerService.getCustomer(receivablePlan.getCustomerId());
+        if(ObjectUtil.isNull(customer)){
+            throw exception(CUSTOMER_NOT_EXISTS);
+        }
+
+    }
+
     @Override
     public void updateReceivablePlan(ReceivablePlanUpdateReqVO updateReqVO) {
         // 校验存在
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
index 39de05fe1..cf216d4d2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableExportReqVO;
@@ -11,10 +12,12 @@ import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePage
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
 import cn.iocoder.yudao.module.crm.enums.AuditStatusEnum;
 import cn.iocoder.yudao.module.crm.service.contract.ContractService;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -23,8 +26,7 @@ import java.util.Collection;
 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.ErrorCodeConstants.RECEIVABLE_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 
 /**
  * 回款管理 Service 实现类
@@ -39,15 +41,21 @@ public class ReceivableServiceImpl implements ReceivableService {
     private ReceivableMapper receivableMapper;
     @Resource
     private ContractService contractService;
+    @Resource
+    private CrmCustomerService crmCustomerService;
 
     @Override
     public Long createReceivable(ReceivableCreateReqVO createReqVO) {
         // TODO @liuhongfeng:planId 是否存在,是否合法,需要去校验;
-        // TODO @liuhongfeng:其它类似 customerId、contractId 也需要去校验;
         // 插入
         ReceivableDO receivable = ReceivableConvert.INSTANCE.convert(createReqVO);
+        if (ObjectUtil.isNull(receivable.getStatus())){
+            receivable.setStatus(CommonStatusEnum.ENABLE.getStatus());
+        }
+        if (ObjectUtil.isNull(receivable.getCheckStatus())){
+            receivable.setCheckStatus(AuditStatusEnum.AUDIT_NEW.getValue());
+        }
 
-        receivable.setCheckStatus(AuditStatusEnum.AUDIT_NEW.getValue());
         //校验
         checkReceivable(receivable);
 
@@ -67,6 +75,11 @@ public class ReceivableServiceImpl implements ReceivableService {
             throw exception(CONTRACT_NOT_EXISTS);
         }
 
+        CrmCustomerDO customer = crmCustomerService.getCustomer(receivable.getCustomerId());
+        if(ObjectUtil.isNull(customer)){
+            throw exception(CUSTOMER_NOT_EXISTS);
+        }
+
     }
 
     @Override

From 67ac11b56c242df634a000315c8265e43fb2552a Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 29 Oct 2023 18:39:54 +0800
Subject: [PATCH 035/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E8=BD=AC=E7=A7=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../yudao/module/crm/enums/ErrorCodeConstants.java    |  6 +++---
 .../admin/business/vo/CrmBusinessCreateReqVO.java     |  9 ---------
 .../admin/business/vo/CrmBusinessTransferReqVO.java   |  2 +-
 .../admin/business/vo/CrmBusinessUpdateReqVO.java     |  8 --------
 .../admin/contact/vo/ContactUpdateReqVO.java          |  8 --------
 .../admin/contact/vo/CrmContactTransferReqVO.java     |  2 +-
 .../controller/admin/contract/ContractController.java |  1 -
 .../admin/contract/vo/ContractCreateReqVO.java        |  8 --------
 .../admin/contract/vo/ContractUpdateReqVO.java        |  7 -------
 .../admin/contract/vo/CrmContractTransferReqVO.java   |  2 +-
 .../crm/convert/business/CrmBusinessConvert.java      |  4 +++-
 .../module/crm/convert/contact/ContactConvert.java    |  1 +
 .../module/crm/convert/contract/ContractConvert.java  |  1 +
 .../yudao/module/crm/framework/utils/AuthUtil.java    |  4 ++++
 .../crm/service/business/CrmBusinessServiceImpl.java  | 11 ++++++-----
 .../crm/service/contact/ContactServiceImpl.java       |  1 +
 .../crm/service/contract/ContractServiceImpl.java     |  1 +
 17 files changed, 23 insertions(+), 53 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 7c1f351d8..69405a5b9 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -11,7 +11,7 @@ public interface ErrorCodeConstants {
 
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
-    ErrorCode CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_000_001, "合同转移失败,原因:没有转移权限");
+    ErrorCode CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_000_001, "合同转移失败,原因:没有转移权限"); // TODO @puhui999:这个搞成 “合同操作失败,原因:没有权限”
     ErrorCode CONTRACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_000_002, "合同转移失败,原因:负责人不存在");
 
     // ========== 线索管理 1-020-001-000 ==========
@@ -19,7 +19,7 @@ public interface ErrorCodeConstants {
 
     // ========== 商机管理 1-020-002-000 ==========
     ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
-    ErrorCode BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_002_001, "商机转移失败,原因:没有转移权限");
+    ErrorCode BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_002_001, "商机转移失败,原因:没有转移权限"); // TODO @puhui999:这个搞成 “商机操作失败,原因:没有权限”
     ErrorCode BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_002_002, "商机转移失败,原因:负责人不存在");
 
     // TODO @lilleo:商机状态、商机类型,都单独错误码段
@@ -29,7 +29,7 @@ public interface ErrorCodeConstants {
 
     // ========== 联系人管理 1-020-003-000 ==========
     ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
-    ErrorCode CONTACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_003_001, "联系人转移失败,原因:没有转移权限");
+    ErrorCode CONTACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_003_001, "联系人转移失败,原因:没有转移权限"); // TODO @puhui999:这个搞成 “联系人操作失败,原因:没有权限”
     ErrorCode CONTACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_003_002, "联系人转移失败,原因:负责人不存在");
 
     // TODO @liuhongfeng:错误码分段;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
index 968a105c8..f743c8469 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
@@ -5,21 +5,12 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-import java.util.Set;
-
 @Schema(description = "管理后台 - 商机创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class CrmBusinessCreateReqVO extends CrmBusinessBaseVO {
 
-    @Schema(description = "只读权限的用户编号数组")
-    private Set<Long> roUserIds;
-
-    @Schema(description = "读写权限的用户编号数组")
-    private Set<Long> rwUserIds;
-
-
     // TODO @ljileo:新建的时候,应该可以传递添加的产品;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
index 993e4750e..fd769e77c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
@@ -15,6 +15,6 @@ public class CrmBusinessTransferReqVO {
 
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId; // 新的负责人
+    private Long ownerUserId;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
index 2929535cf..f137d4c5b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
@@ -6,7 +6,6 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 import javax.validation.constraints.NotNull;
-import java.util.Set;
 
 @Schema(description = "管理后台 - 商机更新 Request VO")
 @Data
@@ -18,13 +17,6 @@ public class CrmBusinessUpdateReqVO extends CrmBusinessBaseVO {
     @NotNull(message = "主键不能为空")
     private Long id;
 
-    @Schema(description = "只读权限的用户编号数组")
-    private Set<Long> roUserIds;
-
-    @Schema(description = "读写权限的用户编号数组")
-    private Set<Long> rwUserIds;
-
-
     // TODO @ljileo:修改的时候,应该可以传递添加的产品;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
index 60afd06f2..f319b52f0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
@@ -5,8 +5,6 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-import java.util.Set;
-
 @Schema(description = "管理后台 - crm联系人更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -16,10 +14,4 @@ public class ContactUpdateReqVO extends ContactBaseVO {
     @Schema(description = "主键", example = "23210")
     private Long id;
 
-    @Schema(description = "只读权限的用户编号数组")
-    private Set<Long> roUserIds;
-
-    @Schema(description = "读写权限的用户编号数组")
-    private Set<Long> rwUserIds;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
index 90acc5645..de4c1cbb6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
@@ -15,6 +15,6 @@ public class CrmContactTransferReqVO {
 
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId; // 新的负责人
+    private Long ownerUserId;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
index 4898eb6c1..7a7326cde 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
@@ -95,5 +95,4 @@ public class ContractController {
         return success(true);
     }
 
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
index 7b8c561b0..b21007f47 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
@@ -5,18 +5,10 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-import java.util.Set;
-
 @Schema(description = "管理后台 - 合同创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class ContractCreateReqVO extends ContractBaseVO {
 
-    @Schema(description = "只读权限的用户编号数组")
-    private Set<Long> roUserIds;
-
-    @Schema(description = "读写权限的用户编号数组")
-    private Set<Long> rwUserIds;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
index e6f305ce4..ba38ca383 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
@@ -6,7 +6,6 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 import javax.validation.constraints.NotNull;
-import java.util.Set;
 
 @Schema(description = "管理后台 - 合同更新 Request VO")
 @Data
@@ -18,10 +17,4 @@ public class ContractUpdateReqVO extends ContractBaseVO {
     @NotNull(message = "合同编号不能为空")
     private Long id;
 
-    @Schema(description = "只读权限的用户编号数组")
-    private Set<Long> roUserIds;
-
-    @Schema(description = "读写权限的用户编号数组")
-    private Set<Long> rwUserIds;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
index 5dfa5fe0a..b02834932 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
@@ -15,6 +15,6 @@ public class CrmContractTransferReqVO {
 
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId; // 新的负责人
+    private Long ownerUserId;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index 2eabf7d4d..a025d3ed4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -32,8 +32,10 @@ public interface CrmBusinessConvert {
 
     default CrmBusinessDO convert(CrmBusinessDO business, CrmBusinessTransferReqVO reqVO, Long userId) {
         Set<Long> rwUserIds = business.getRwUserIds();
-        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
+        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人 TODO puhui999:是不是直接 rwUserIds.remove(userId)
+        // TODO @puhui999:ownerUserId 不用添加到进去,它就是 ownerUserId 就够;因为一共有 3 个角色:负责人、读写、只读;
         rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
+        // TODO @puhui999:对原负责人,加个类似的处理:移除、转化为团队成员(只读、读写)
         return new CrmBusinessDO().setId(business.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
                 .setRwUserIds(rwUserIds);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index 0d1329819..c14ea7586 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -32,6 +32,7 @@ public interface ContactConvert {
 
     List<ContactExcelVO> convertList02(List<ContactDO> list);
 
+    // TODO @puhui999:参考 CrmBusinessConvert 的修改建议
     default ContactDO convert(ContactDO contact, CrmContactTransferReqVO reqVO, Long userId) {
         Set<Long> rwUserIds = contact.getRwUserIds();
         rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index d7547d4b8..7d0dc84fa 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -32,6 +32,7 @@ public interface ContractConvert {
 
     List<ContractExcelVO> convertList02(List<ContractDO> list);
 
+    // TODO @puhui999:参考 CrmBusinessConvert 的修改建议
     default ContractDO convert(ContractDO contract, CrmContractTransferReqVO reqVO, Long userId) {
         Set<Long> rwUserIds = contract.getRwUserIds();
         rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java
index 3c3d14bf2..fb66d6f0d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.framework.utils;
 
 import java.util.Collection;
 
+// TODO @puhui999:改成 CrmPermissionUtils;
 /**
  * 数据读写权限校验工具类
  *
@@ -9,6 +10,8 @@ import java.util.Collection;
  */
 public class AuthUtil {
 
+    // TODO @puhui999:负责人是单独的字段哈;
+    // TODO @puhui999:额外校验,如果是管理员,可以查看所有;看着要做成有状态的了,可能要搞个 CrmPermissionService 咧;
     /**
      * 判断当前数据对用户来说是否是只读的
      *
@@ -17,6 +20,7 @@ public class AuthUtil {
      * @return boolean 是/否
      */
     public static boolean isReadOnly(Collection<Long> roUserIds, Long userId) {
+        // TODO @puhui999:从代码角度来说,最好使用 CollUtil.contains
         return roUserIds.contains(userId);
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index 4ddb3363e..fc01ec555 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -92,26 +92,27 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         return businessMapper.selectList(exportReqVO);
     }
 
+    // TODO @puhui999:动名词哈。transferBusiness
     @Override
     public void businessTransfer(CrmBusinessTransferReqVO reqVO, Long userId) {
-        // 1. 校验商机是否存在
+        // 1.1 校验商机是否存在
         CrmBusinessDO business = validateBusinessExists(reqVO.getId());
-        // 1.2. 校验用户是否拥有读写权限
+        // 1.2 校验用户是否拥有读写权限
         if (!isReadAndWrite(business.getRwUserIds(), userId)) {
             throw exception(BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED);
         }
-        // 2. 校验新负责人是否存在
+        // TODO @puhui999:如果已经是该负责人,抛个业务异常;
+        // 1.3 校验新负责人是否存在
         AdminUserRespDTO user = adminUserApi.getUser(reqVO.getOwnerUserId());
         if (user == null) {
             throw exception(BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS);
         }
 
-        // 3. 更新新的负责人
+        // 2. 更新新的负责人
         CrmBusinessDO updateBusiness = CrmBusinessConvert.INSTANCE.convert(business, reqVO, userId);
         businessMapper.updateById(updateBusiness);
 
         // 4. TODO 记录商机转移日志
-
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index e47e2e155..cca752efa 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -95,6 +95,7 @@ public class ContactServiceImpl implements ContactService {
         return contactMapper.selectList(exportReqVO);
     }
 
+    // TODO @puhui999:参考 CrmBusinessServiceImpl 修改建议
     @Override
     public void contactTransfer(CrmContactTransferReqVO reqVO, Long userId) {
         // 1. 校验联系人是否存在
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index 8cee161f0..05e1dadd7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -92,6 +92,7 @@ public class ContractServiceImpl implements ContractService {
         return contractMapper.selectList(exportReqVO);
     }
 
+    // TODO @puhui999:参考 CrmBusinessServiceImpl 修改建议
     @Override
     public void contractTransfer(CrmContractTransferReqVO reqVO, Long userId) {
         // 1. 校验合同是否存在

From d1978d318bed325332ca65d0ced2b008a0fee58d Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 29 Oct 2023 19:05:36 +0800
Subject: [PATCH 036/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E5=90=88?=
 =?UTF-8?q?=E5=90=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             | 37 ------------
 sql/mysql/crm_menu.sql                        | 59 -------------------
 .../admin/business/vo/CrmBusinessExcelVO.java | 21 +++----
 3 files changed, 7 insertions(+), 110 deletions(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 677ea2881..333e5be23 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -1,42 +1,5 @@
 SET NAMES utf8mb4;
 
--- ----------------------------
--- 合同表
--- ----------------------------
-DROP TABLE IF EXISTS `crm_contract`;
-CREATE TABLE `crm_contract`
-(
-    `id`                  bigint                                                        NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
-    `name`                varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合同名称',
-    `customer_id`         bigint                                                                 DEFAULT NULL COMMENT '客户编号',
-    `business_id`         bigint                                                                 DEFAULT NULL COMMENT '商机编号',
-    `process_instance_id` bigint                                                                 DEFAULT NULL COMMENT '工作流编号',
-    `order_date`          datetime                                                               DEFAULT NULL COMMENT '下单日期',
-    `owner_user_id`       bigint                                                                 DEFAULT NULL COMMENT '负责人的用户编号',
-    `no`                  varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci          DEFAULT NULL COMMENT '合同编号',
-    `start_time`          datetime                                                               DEFAULT NULL COMMENT '开始时间',
-    `end_time`            datetime                                                               DEFAULT NULL COMMENT '结束时间',
-    `price`               int                                                                    DEFAULT NULL COMMENT '合同金额',
-    `discount_percent`    int                                                                    DEFAULT NULL COMMENT '整单折扣',
-    `product_price`       int                                                                    DEFAULT NULL COMMENT '产品总金额',
-    `ro_user_ids`         varchar(4096)                                                          DEFAULT NULL COMMENT '只读权限的用户编号数组',
-    `rw_user_ids`         varchar(4096)                                                          DEFAULT NULL COMMENT '读写权限的用户编号数组',
-    `contact_id`          bigint                                                                 DEFAULT NULL COMMENT '联系人编号',
-    `sign_user_id`        bigint                                                                 DEFAULT NULL COMMENT '公司签约人',
-    `contact_last_time`   datetime                                                               DEFAULT NULL COMMENT '最后跟进时间',
-
-    -- 通用字段
-    `remark`              varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL     DEFAULT NULL COMMENT '备注',
-    `creator`             varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci  NULL     DEFAULT '' COMMENT '创建者',
-    `create_time`         datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `updater`             varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci  NULL     DEFAULT '' COMMENT '更新者',
-    `update_time`         datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    `deleted`             bit(1)                                                        NOT NULL DEFAULT b'0' COMMENT '是否删除',
-    `tenant_id`           bigint                                                        NOT NULL DEFAULT 0 COMMENT '租户编号',
-    PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB
-  CHARACTER SET = utf8mb4
-  COLLATE = utf8mb4_unicode_ci COMMENT ='合同表';
 
 
 DROP TABLE IF EXISTS `crm_clue`;
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index d0c9b6dce..ade69c658 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -1,62 +1,3 @@
--- ----------------------------
--- 合同菜单
--- ----------------------------
-
--- 菜单 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status, component_name
-)
-VALUES (
-    '合同管理', '', 2, 0, 2375,
-    'contract', '', 'crm/contract/index', 0, 'Contract'
-);
-
--- 按钮父菜单ID
--- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
-SELECT @parentId := LAST_INSERT_ID();
-
--- 按钮 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-    '合同查询', 'crm:contract:query', 3, 1, @parentId,
-    '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-    '合同创建', 'crm:contract:create', 3, 2, @parentId,
-    '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-    '合同更新', 'crm:contract:update', 3, 3, @parentId,
-    '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-    '合同删除', 'crm:contract:delete', 3, 4, @parentId,
-    '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-    '合同导出', 'crm:contract:export', 3, 5, @parentId,
-    '', '', '', 0
-);
 
 -- ----------------------------
 -- 线索菜单
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java
index 087deafb0..e7e3ef987 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java
@@ -1,18 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
 
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.math.BigDecimal;
-import java.math.BigDecimal;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-
 import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Set;
 
 /**
  * 商机 Excel VO
@@ -62,10 +55,10 @@ public class CrmBusinessExcelVO {
     private LocalDateTime createTime;
 
     @ExcelProperty("只读权限的用户编号数组")
-    private String roUserIds;
+    private Set<Long> roUserIds;
 
     @ExcelProperty("读写权限的用户编号数组")
-    private String rwUserIds;
+    private Set<Long> rwUserIds;
 
     @ExcelProperty("1赢单2输单3无效")
     private Integer endStatus;

From 973b7109927b0803040045320b2ddcf70468b4b0 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 29 Oct 2023 19:14:15 +0800
Subject: [PATCH 037/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E5=95=86?=
 =?UTF-8?q?=E6=9C=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql      |  87 -------------------------------
 sql/mysql/crm_menu.sql | 116 -----------------------------------------
 2 files changed, 203 deletions(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 333e5be23..e5b120ead 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -2,63 +2,6 @@ SET NAMES utf8mb4;
 
 
 
-DROP TABLE IF EXISTS `crm_clue`;
-CREATE TABLE `crm_clue`  (
-                             `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,主键自增',
-                             `transform_status` tinyint DEFAULT NULL COMMENT '转化状态',
-                             `follow_up_status` tinyint DEFAULT NULL COMMENT '跟进状态',
-                             `name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '线索名称',
-                             `customer_id` bigint NOT NULL COMMENT '客户id',
-                             `contact_next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
-                             `telephone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
-                             `mobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
-                             `address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
-                             `owner_user_id` bigint NOT NULL COMMENT '负责人的用户编号',
-                             `contact_last_time` datetime DEFAULT NULL COMMENT '最后跟进时间',
-                             `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
-                             `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
-                             `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-                             `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
-                             `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-                             `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-                             `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-                             PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB COMMENT = '线索表' ;
-
--- ----------------------------
--- 商机表
--- ----------------------------
-
-DROP TABLE IF EXISTS `crm_business`;
-CREATE TABLE `crm_business` (
-                                `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
-                                `name` varchar(100) NOT NULL COMMENT '商机名称',
-                                `status_type_id` bigint DEFAULT NULL COMMENT '商机状态类型编号',
-                                `status_id` bigint DEFAULT NULL COMMENT '商机状态编号',
-                                `contact_next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
-                                `customer_id` bigint NOT NULL COMMENT '客户编号',
-                                `deal_time` datetime DEFAULT NULL COMMENT '预计成交日期',
-                                `price` decimal(18,2) DEFAULT NULL COMMENT '商机金额',
-                                `discount_percent` decimal(10,2) DEFAULT NULL COMMENT '整单折扣',
-                                `product_price` decimal(18,2) DEFAULT NULL COMMENT '产品总金额',
-                                `remark` varchar(500) DEFAULT NULL COMMENT '备注',
-                                `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '创建人',
-                                `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '更新人',
-                                `owner_user_id` bigint DEFAULT NULL COMMENT '负责人的用户编号',
-                                `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-                                `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
-                                `ro_user_ids` longtext NOT NULL COMMENT '只读权限的用户编号数组',
-                                `rw_user_ids` longtext NOT NULL COMMENT '读写权限的用户编号数组',
-                                `end_status` int NOT NULL COMMENT '1赢单2输单3无效',
-                                `end_remark` varchar(500) DEFAULT NULL COMMENT '结束时的备注',
-                                `deleted` bit(1) DEFAULT b'0' COMMENT '逻辑删除',
-                                `contact_last_time` datetime DEFAULT NULL COMMENT '最后跟进时间',
-                                `follow_up_status` int DEFAULT NULL COMMENT '跟进状态',
-                                `tenant_id` bigint DEFAULT '0' COMMENT '租户ID',
-                                PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商机表';
-
-
 
 -- ----------------------------
 -- 回款表
@@ -148,33 +91,3 @@ CREATE TABLE `crm_contact` (
                                PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='crm联系人';
 
-
--- ----------------------------
--- 商机状态表
--- ----------------------------
-DROP TABLE IF EXISTS `crm_business_status`;
-CREATE TABLE `crm_business_status` (
-                                       `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
-                                       `type_id` bigint NOT NULL COMMENT '状态类型编号',
-                                       `name` varchar(100) NOT NULL COMMENT '状态名',
-                                       `percent` varchar(20) DEFAULT NULL COMMENT '赢单率',
-                                       `sort` int DEFAULT NULL COMMENT '排序',
-                                       `tenant_id` bigint DEFAULT '1' COMMENT '租户ID',
-                                       PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商机状态表'
-
--- ----------------------------
--- 商机状态类型表
--- ----------------------------
-DROP TABLE IF EXISTS `crm_business_status_type`;
-CREATE TABLE `crm_business_status_type` (
-                                            `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
-                                            `name` varchar(100) NOT NULL COMMENT '状态类型名',
-                                            `dept_ids` varchar(200) NOT NULL COMMENT '使用的部门编号',
-                                            `status` bit(1) NOT NULL DEFAULT b'0' COMMENT '开启状态',
-                                            `creator` varchar(64) NOT NULL COMMENT '创建人',
-                                            `create_time` datetime NOT NULL COMMENT '创建时间',
-                                            `update_time` datetime DEFAULT NULL COMMENT '更新时间',
-                                            `tenant_id` bigint DEFAULT '1' COMMENT '租户ID',
-                                            PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商机状态类型表'
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index ade69c658..be0595da1 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -1,63 +1,6 @@
 
--- ----------------------------
--- 线索菜单
--- ----------------------------
 
--- 菜单 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status, component_name
-)
-VALUES (
-   '线索管理', '', 2, 0, 2375,
-   'clue', '', 'crm/clue/index', 0, 'CrmClue'
-);
 
--- 按钮父菜单ID
--- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
-SELECT @parentId := LAST_INSERT_ID();
-
--- 按钮 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '线索查询', 'crm:clue:query', 3, 1, @parentId,
-   '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '线索创建', 'crm:clue:create', 3, 2, @parentId,
-   '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '线索更新', 'crm:clue:update', 3, 3, @parentId,
-   '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '线索删除', 'crm:clue:delete', 3, 4, @parentId,
-   '', '', '', 0
-);
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-   '线索导出', 'crm:clue:export', 3, 5, @parentId,
-   '', '', '', 0
-);
 -- 菜单 SQL
 INSERT INTO system_menu(
     name, permission, type, sort, parent_id,
@@ -114,65 +57,6 @@ VALUES (
            '', '', '', 0
        );
 
--- ----------------------------
--- 合同菜单
--- ----------------------------
-
--- 菜单 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status, component_name
-)
-VALUES (
-           '商机管理', '', 2, 0, '',
-           'business', '', 'crm/business/index', 0, 'CrmBusiness'
-       );
-
--- 按钮父菜单ID
--- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
-SELECT @parentId := LAST_INSERT_ID();
-
--- 按钮 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '商机查询', 'crm:business:query', 3, 1, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '商机创建', 'crm:business:create', 3, 2, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '商机更新', 'crm:business:update', 3, 3, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '商机删除', 'crm:business:delete', 3, 4, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '商机导出', 'crm:business:export', 3, 5, @parentId,
-           '', '', '', 0
-       );
 
 -- ----------------------------
 -- 回款菜单

From 53afce0bff2cef4d2dcbe2b9f5487825ee13bdfe Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 29 Oct 2023 19:22:09 +0800
Subject: [PATCH 038/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E5=9B=9E?=
 =?UTF-8?q?=E6=AC=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql      |  28 -------
 sql/mysql/crm_menu.sql | 180 -----------------------------------------
 2 files changed, 208 deletions(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index e5b120ead..de7922d24 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -63,31 +63,3 @@ CREATE TABLE `crm_receivable_plan`  (
     `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
     PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回款计划' ROW_FORMAT = DYNAMIC;
-
-
-
-
-
-
-CREATE TABLE `crm_contact` (
-                               `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
-                               `name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系人名称',
-                               `next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
-                               `mobile` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
-                               `telephone` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
-                               `email` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电子邮箱',
-                               `post` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '职务',
-                               `customer_id` bigint(20) DEFAULT NULL COMMENT '客户编号',
-                               `address` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
-                               `remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
-                               `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人',
-                               `owner_user_id` bigint DEFAULT NULL COMMENT '负责人用户编号',
-                               `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
-                               `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
-                               `last_time` timestamp NULL DEFAULT NULL COMMENT '最后跟进时间',
-                               `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人',
-                               `deleted` bit(1) NOT NULL DEFAULT b'0',
-                               `tenant_id` bigint DEFAULT NULL,
-                               PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='crm联系人';
-
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index be0595da1..e69de29bb 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -1,180 +0,0 @@
-
-
-
--- 菜单 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status, component_name
-)
-VALUES (
-           '联系人', '', 2, 0, ${table.parentMenuId},
-           'contact', '', 'crm/contact/index', 0, 'Contact'
-       );
-
--- 按钮父菜单ID
--- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
-SELECT @parentId := LAST_INSERT_ID();
-
--- 按钮 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '联系人查询', 'crm:contact:query', 3, 1, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '联系人创建', 'crm:contact:create', 3, 2, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '联系人更新', 'crm:contact:update', 3, 3, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '联系人删除', 'crm:contact:delete', 3, 4, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '联系人导出', 'crm:contact:export', 3, 5, @parentId,
-           '', '', '', 0
-       );
-
-
--- ----------------------------
--- 回款菜单
--- ----------------------------
-
--- 菜单 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status, component_name
-)
-VALUES (
-           '回款管理', '', 2, 0, 2375,
-           'receivable', '', 'crm/receivable/index', 0, 'Receivable'
-       );
-
--- 按钮父菜单ID
--- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
-SELECT @parentId := LAST_INSERT_ID();
-
--- 按钮 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款管理查询', 'crm:receivable:query', 3, 1, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款管理创建', 'crm:receivable:create', 3, 2, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款管理更新', 'crm:receivable:update', 3, 3, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款管理删除', 'crm:receivable:delete', 3, 4, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款管理导出', 'crm:receivable:export', 3, 5, @parentId,
-           '', '', '', 0
-       );
-
-
--- ----------------------------
--- 回款计划菜单
--- ----------------------------
-
--- 菜单 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status, component_name
-)
-VALUES (
-           '回款计划管理', '', 2, 0, 2375,
-           'receivable-plan', '', 'crm/receivablePlan/index', 0, 'ReceivablePlan'
-       );
-
--- 按钮父菜单ID
--- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
-SELECT @parentId := LAST_INSERT_ID();
-
--- 按钮 SQL
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款计划查询', 'crm:receivable-plan:query', 3, 1, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款计划创建', 'crm:receivable-plan:create', 3, 2, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款计划更新', 'crm:receivable-plan:update', 3, 3, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款计划删除', 'crm:receivable-plan:delete', 3, 4, @parentId,
-           '', '', '', 0
-       );
-INSERT INTO system_menu(
-    name, permission, type, sort, parent_id,
-    path, icon, component, status
-)
-VALUES (
-           '回款计划导出', 'crm:receivable-plan:export', 3, 5, @parentId,
-           '', '', '', 0
-       );

From 054d80b4725eafd88dee23b563094737b8aa4a30 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 29 Oct 2023 19:23:09 +0800
Subject: [PATCH 039/101] =?UTF-8?q?code=20review=EF=BC=9A=E5=90=8C?=
 =?UTF-8?q?=E6=AD=A5=20crm=20=E6=9C=80=E6=96=B0=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/ruoyi-vue-pro.sql | 112 +++++++++++++++++++++++++++++++-----
 1 file changed, 97 insertions(+), 15 deletions(-)

diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql
index 0022811bb..b26915d5e 100644
--- a/sql/mysql/ruoyi-vue-pro.sql
+++ b/sql/mysql/ruoyi-vue-pro.sql
@@ -11,7 +11,7 @@
  Target Server Version : 80034
  File Encoding         : 65001
 
- Date: 24/10/2023 20:48:38
+ Date: 29/10/2023 19:22:42
 */
 
 SET NAMES utf8mb4;
@@ -341,7 +341,8 @@ CREATE TABLE `infra_api_access_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_create_time`(`create_time` ASC) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 35832 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'API 访问日志表';
 
 -- ----------------------------
@@ -384,7 +385,7 @@ CREATE TABLE `infra_api_error_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1745 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 1750 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
 
 -- ----------------------------
 -- Records of infra_api_error_log
@@ -538,7 +539,7 @@ CREATE TABLE `infra_file`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1108 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1109 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
 
 -- ----------------------------
 -- Records of infra_file
@@ -587,7 +588,7 @@ CREATE TABLE `infra_file_content`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 201 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
+) ENGINE = InnoDB AUTO_INCREMENT = 202 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
 
 -- ----------------------------
 -- Records of infra_file_content
@@ -768,7 +769,7 @@ CREATE TABLE `member_experience_record`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_user_id`(`user_id` ASC) USING BTREE COMMENT '会员经验记录-用户编号',
   INDEX `idx_user_biz_type`(`user_id` ASC, `biz_type` ASC) USING BTREE COMMENT '会员经验记录-用户业务类型'
-) ENGINE = InnoDB AUTO_INCREMENT = 41 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员经验记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员经验记录';
 
 -- ----------------------------
 -- Records of member_experience_record
@@ -884,7 +885,7 @@ CREATE TABLE `member_level_record`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_user_id`(`user_id` ASC) USING BTREE COMMENT '会员等级记录-用户编号'
-) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 21 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级记录';
 
 -- ----------------------------
 -- Records of member_level_record
@@ -914,7 +915,7 @@ CREATE TABLE `member_point_record`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `index_userId`(`user_id` ASC) USING BTREE,
   INDEX `index_title`(`title` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 60 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户积分记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户积分记录';
 
 -- ----------------------------
 -- Records of member_point_record
@@ -1434,6 +1435,39 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1395, 22, '订单积分奖励(整单取消)', '22', 'member_point_biz_type', 0, 'default', '', '', '1', '2023-10-11 07:42:55', '1', '2023-10-11 07:43:01', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1396, 23, '订单积分奖励(单个退款)', '23', 'member_point_biz_type', 0, 'default', '', '', '1', '2023-10-11 07:43:16', '1', '2023-10-11 07:43:16', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1397, 13, '下单奖励(单个退款)', '13', 'member_experience_biz_type', 0, 'warning', '', '', '1', '2023-10-11 07:45:24', '1', '2023-10-11 07:45:38', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1402, 1, 'A 农、林、牧、渔业', '1', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:15', '1', '2023-10-28 23:02:15', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1403, 2, 'B 采矿业', '2', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:29', '1', '2023-10-28 23:02:29', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1404, 3, 'C 制造业', '3', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:41', '1', '2023-10-28 23:02:41', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1405, 4, 'D 电力、热力、燃气及水生产和供应业', '4', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:02:54', '1', '2023-10-28 23:02:54', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1406, 5, 'E 建筑业', '5', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:03', '1', '2023-10-28 23:03:03', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1407, 6, 'F 批发和零售业', '6', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:13', '1', '2023-10-28 23:03:13', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1408, 7, 'G 交通运输、仓储和邮政业', '7', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:27', '1', '2023-10-28 23:03:27', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1409, 8, 'H 住宿和餐饮业', '8', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:37', '1', '2023-10-28 23:03:37', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1410, 9, 'I 信息传输、软件和信息技术服务业', '9', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:47', '1', '2023-10-28 23:03:47', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1411, 10, 'J 金融业', '10', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:03:57', '1', '2023-10-28 23:03:57', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1412, 11, 'K 房地产业', '11', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:15', '1', '2023-10-28 23:04:22', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1413, 12, 'L 租赁和商务服务业', '12', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:33', '1', '2023-10-28 23:04:33', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1414, 13, 'M 科学研究和技术服务业', '13', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:43', '1', '2023-10-28 23:04:43', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1415, 14, 'N 水利、环境和公共设施管理业', '14', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:04:53', '1', '2023-10-28 23:04:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1416, 15, 'O 居民服务、修理和其他服务业', '15', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:05', '1', '2023-10-28 23:05:05', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1417, 16, 'P 教育', '16', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:15', '1', '2023-10-28 23:05:15', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1418, 17, 'Q 卫生和社会工作', '17', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:44', '1', '2023-10-28 23:05:44', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1419, 18, 'R 文化、体育和娱乐业', '18', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:05:55', '1', '2023-10-28 23:05:55', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1420, 19, 'S 公共管理、社会保障和社会组织', '19', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:06:05', '1', '2023-10-28 23:06:05', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1421, 20, 'T 国际组织', '20', 'crm_customer_industry', 0, 'default', '', '', '1', '2023-10-28 23:06:15', '1', '2023-10-28 23:06:15', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1422, 1, 'A (重点客户)', '1', 'crm_customer_level', 0, 'primary', '', '', '1', '2023-10-28 23:07:13', '1', '2023-10-28 23:07:13', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1423, 2, 'B (普通客户)', '2', 'crm_customer_level', 0, 'info', '', '', '1', '2023-10-28 23:07:35', '1', '2023-10-28 23:07:35', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1424, 3, 'C (非优先客户)', '3', 'crm_customer_level', 0, 'default', '', '', '1', '2023-10-28 23:07:53', '1', '2023-10-28 23:07:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1425, 1, '促销', '1', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:29', '1', '2023-10-28 23:08:29', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1426, 2, '搜索引擎', '2', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:39', '1', '2023-10-28 23:08:39', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1427, 3, '广告', '3', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:47', '1', '2023-10-28 23:08:47', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1428, 4, '转介绍', '4', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:08:58', '1', '2023-10-28 23:08:58', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1429, 5, '线上注册', '5', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:12', '1', '2023-10-28 23:09:12', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1430, 6, '线上咨询', '6', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:22', '1', '2023-10-28 23:09:22', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1431, 7, '预约上门', '7', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:09:39', '1', '2023-10-28 23:09:39', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1432, 8, '陌拜', '8', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:04', '1', '2023-10-28 23:10:04', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1433, 9, '电话咨询', '9', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:18', '1', '2023-10-28 23:10:18', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1434, 10, '邮件咨询', '10', 'crm_customer_source', 0, 'default', '', '', '1', '2023-10-28 23:10:33', '1', '2023-10-28 23:10:33', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -1454,7 +1488,7 @@ CREATE TABLE `system_dict_type`  (
   `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
   PRIMARY KEY (`id`) USING BTREE,
   UNIQUE INDEX `dict_type`(`type` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 185 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
+) ENGINE = InnoDB AUTO_INCREMENT = 601 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
 
 -- ----------------------------
 -- Records of system_dict_type
@@ -1528,6 +1562,9 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (182, '佣金提现银行', 'brokerage_bank_name', 0, NULL, '', '2023-09-28 02:46:05', '', '2023-09-28 02:46:05', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (183, '砍价记录的状态', 'promotion_bargain_record_status', 0, '', '1', '2023-10-05 10:41:08', '1', '2023-10-05 10:41:08', b'0', '1970-01-01 00:00:00');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (184, '拼团记录的状态', 'promotion_combination_record_status', 0, '', '1', '2023-10-08 07:24:25', '1', '2023-10-08 07:24:25', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (186, '客户所属行业', 'crm_customer_industry', 0, 'CRM 客户所属行业', '1', '2023-10-28 22:57:07', '1', '2023-10-28 15:11:16', b'0', NULL);
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (187, '客户等级', 'crm_customer_level', 0, 'CRM 客户等级', '1', '2023-10-28 22:59:12', '1', '2023-10-28 15:11:16', b'0', NULL);
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (188, '客户来源', 'crm_customer_source', 0, 'CRM 客户来源', '1', '2023-10-28 23:00:34', '1', '2023-10-28 15:11:16', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (600, 'Banner Position', 'promotion_banner_position', 0, '', '1', '2023-10-08 07:24:25', '1', '2023-10-08 07:24:25', b'0', '1970-01-01 00:00:00');
 COMMIT;
 
@@ -1577,7 +1614,7 @@ CREATE TABLE `system_login_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2620 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 2626 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
 
 -- ----------------------------
 -- Records of system_login_log
@@ -2203,6 +2240,49 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2388, '商城首页', '', 2, 1, 2362, 'home', 'ep:home-filled', 'mall/home/index', 'MallHome', 0, b'1', b'1', b'1', '', '2023-10-16 12:10:33', '', '2023-10-16 12:10:33', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2389, '核销订单', '', 2, 2, 2166, 'pick-up-order', 'ep:list', 'mall/trade/delivery/pickUpOrder/index', 'PickUpOrder', 0, b'1', b'1', b'1', '', '2023-10-19 16:09:51', '', '2023-10-19 16:09:51', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2390, '优惠活动', '', 1, 99, 2030, 'youhui', 'ep:aim', '', '', 0, b'1', b'1', b'1', '1', '2023-10-21 19:23:49', '1', '2023-10-21 19:23:49', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2391, '客户管理', '', 2, 0, 2397, 'customer', 'fa:address-book-o', 'crm/customer/index', 'CrmCustomer', 0, b'1', b'1', b'1', '', '2023-10-29 09:04:21', '1', '2023-10-29 17:11:03', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2392, '客户查询', 'crm:customer:query', 3, 1, 2391, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 09:04:21', '', '2023-10-29 09:04:21', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2393, '客户创建', 'crm:customer:create', 3, 2, 2391, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 09:04:21', '', '2023-10-29 09:04:21', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2394, '客户更新', 'crm:customer:update', 3, 3, 2391, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 09:04:21', '', '2023-10-29 09:04:21', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2395, '客户删除', 'crm:customer:delete', 3, 4, 2391, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 09:04:21', '', '2023-10-29 09:04:21', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2396, '客户导出', 'crm:customer:export', 3, 5, 2391, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 09:04:21', '', '2023-10-29 09:04:21', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2397, '客户管理系统', '', 1, 200, 0, '/crm', 'ep:avatar', '', '', 0, b'1', b'1', b'1', '1', '2023-10-29 17:08:30', '1', '2023-10-29 17:08:30', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2398, '合同管理', '', 2, 1, 2397, 'contract', 'ep:notebook', 'crm/contract/index', 'CrmContract', 0, b'1', b'1', b'1', '', '2023-10-29 10:50:41', '1', '2023-10-29 18:55:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2399, '合同查询', 'crm:contract:query', 3, 1, 2398, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 10:50:41', '', '2023-10-29 10:50:41', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2400, '合同创建', 'crm:contract:create', 3, 2, 2398, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 10:50:41', '', '2023-10-29 10:50:41', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2401, '合同更新', 'crm:contract:update', 3, 3, 2398, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 10:50:41', '', '2023-10-29 10:50:41', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2402, '合同删除', 'crm:contract:delete', 3, 4, 2398, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 10:50:41', '', '2023-10-29 10:50:41', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2403, '合同导出', 'crm:contract:export', 3, 5, 2398, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 10:50:41', '', '2023-10-29 10:50:41', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2404, '线索管理', '', 2, 0, 2397, 'clue', 'fa:pagelines', 'crm/clue/index', 'CrmClue', 0, b'1', b'1', b'1', '', '2023-10-29 11:06:29', '1', '2023-10-29 19:08:35', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2405, '线索查询', 'crm:clue:query', 3, 1, 2404, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:06:29', '', '2023-10-29 11:06:29', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2406, '线索创建', 'crm:clue:create', 3, 2, 2404, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:06:29', '', '2023-10-29 11:06:29', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2407, '线索更新', 'crm:clue:update', 3, 3, 2404, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:06:29', '', '2023-10-29 11:06:29', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2408, '线索删除', 'crm:clue:delete', 3, 4, 2404, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:06:29', '', '2023-10-29 11:06:29', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2409, '线索导出', 'crm:clue:export', 3, 5, 2404, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:06:29', '', '2023-10-29 11:06:29', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2410, '商机管理', '', 2, 0, 2397, 'business', 'fa:bus', 'crm/business/index', 'CrmBusiness', 0, b'1', b'1', b'1', '', '2023-10-29 11:12:35', '1', '2023-10-29 19:13:01', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2411, '商机查询', 'crm:business:query', 3, 1, 2410, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:12:35', '', '2023-10-29 11:12:35', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2412, '商机创建', 'crm:business:create', 3, 2, 2410, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:12:35', '', '2023-10-29 11:12:35', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2413, '商机更新', 'crm:business:update', 3, 3, 2410, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:12:35', '', '2023-10-29 11:12:35', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2414, '商机删除', 'crm:business:delete', 3, 4, 2410, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:12:35', '', '2023-10-29 11:12:35', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2415, '商机导出', 'crm:business:export', 3, 5, 2410, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:12:35', '', '2023-10-29 11:12:35', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2416, '联系人管理', '', 2, 0, 2397, 'contact', 'fa:address-book-o', 'crm/contact/index', 'Contact', 0, b'1', b'1', b'1', '', '2023-10-29 11:14:56', '1', '2023-10-29 19:15:32', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2417, '联系人查询', 'crm:contact:query', 3, 1, 2416, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:14:56', '', '2023-10-29 11:14:56', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2418, '联系人创建', 'crm:contact:create', 3, 2, 2416, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:14:56', '', '2023-10-29 11:14:56', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2419, '联系人更新', 'crm:contact:update', 3, 3, 2416, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:14:56', '', '2023-10-29 11:14:56', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2420, '联系人删除', 'crm:contact:delete', 3, 4, 2416, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:14:56', '', '2023-10-29 11:14:56', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2421, '联系人导出', 'crm:contact:export', 3, 5, 2416, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:14:56', '', '2023-10-29 11:14:56', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2422, '回款管理', '', 2, 0, 2397, 'receivable', 'ep:money', 'crm/receivable/index', 'CrmReceivable', 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '1', '2023-10-29 19:18:52', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2423, '回款管理查询', 'crm:receivable:query', 3, 1, 2422, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2424, '回款管理创建', 'crm:receivable:create', 3, 2, 2422, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2425, '回款管理更新', 'crm:receivable:update', 3, 3, 2422, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2426, '回款管理删除', 'crm:receivable:delete', 3, 4, 2422, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2427, '回款管理导出', 'crm:receivable:export', 3, 5, 2422, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2428, '回款计划管理', '', 2, 0, 2397, 'receivable-plan', 'fa:money', 'crm/receivablePlan/index', 'CrmReceivablePlan', 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '1', '2023-10-29 19:19:08', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2429, '回款计划查询', 'crm:receivable-plan:query', 3, 1, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2430, '回款计划创建', 'crm:receivable-plan:create', 3, 2, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2431, '回款计划更新', 'crm:receivable-plan:update', 3, 3, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2432, '回款计划删除', 'crm:receivable-plan:delete', 3, 4, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2433, '回款计划导出', 'crm:receivable-plan:export', 3, 5, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -2320,8 +2400,10 @@ CREATE TABLE `system_oauth2_access_token`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3130 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
+  INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3137 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_access_token
@@ -2443,7 +2525,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1089 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 1094 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_refresh_token
@@ -2483,7 +2565,7 @@ CREATE TABLE `system_operate_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 8757 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 8766 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
 
 -- ----------------------------
 -- Records of system_operate_log
@@ -3834,7 +3916,7 @@ CREATE TABLE `system_users`  (
 -- Records of system_users
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac4b608e151e62a74d9633f15d085aee026eedeb60.png', 0, '0:0:0:0:0:0:0:1', '2023-10-24 20:20:54', 'admin', '2021-01-05 17:03:47', NULL, '2023-10-24 20:20:54', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac4b608e151e62a74d9633f15d085aee026eedeb60.png', 0, '0:0:0:0:0:0:0:1', '2023-10-29 17:06:12', 'admin', '2021-01-05 17:03:47', NULL, '2023-10-29 17:06:12', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '127.0.0.1', '2022-07-08 01:26:27', '', '2021-01-13 23:50:35', NULL, '2022-07-08 01:26:27', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$10$GP8zvqHB//TekuzYZSBYAuBQJiNq1.fxQVDYJ.uBCOnWCtDVKE4H6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2023-09-24 18:21:19', '', '2021-01-21 02:13:53', NULL, '2023-09-24 18:21:19', b'0', 1);

From bec8a27a21780c86c5b6d2c357a5522c61e42090 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Mon, 30 Oct 2023 00:49:36 +0800
Subject: [PATCH 040/101] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20CRM-=E6=95=B0?=
 =?UTF-8?q?=E6=8D=AE=E6=9D=83=E9=99=90=20=E6=B3=A8=E8=A7=A3=E5=AE=9E?=
 =?UTF-8?q?=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |   5 +-
 .../admin/business/CrmBusinessController.http |  11 ++
 .../admin/business/CrmBusinessController.java |   6 +-
 .../business/vo/CrmBusinessTransferReqVO.java |  20 ---
 .../business/vo/CrmTransferBusinessReqVO.java |  11 ++
 .../convert/business/CrmBusinessConvert.java  |   8 +-
 .../crm/convert/contract/ContractConvert.java |   2 +-
 .../dataobject/business/CrmBusinessDO.java    |  21 +--
 .../crm/dal/dataobject/contact/ContactDO.java |  24 +--
 .../dal/dataobject/contract/ContractDO.java   |  21 +--
 .../dataobject/customer/CrmCustomerDO.java    |  21 +--
 .../core/annotations/CrmPermission.java       |  34 ++++
 .../core/aop/CrmPermissionAspect.java         | 165 ++++++++++++++++++
 .../crm/framework/core/package-info.java      |   1 +
 .../dataobject/CrmPermissionBaseDO.java       |  37 ++++
 .../module/crm/framework/enums/CrmEnum.java   |  34 ++++
 .../framework/enums/OperationTypeEnum.java    |  39 +++++
 ...{AuthUtil.java => CrmPermissionUtils.java} |  21 ++-
 .../crm/framework/vo/CrmTransferBaseVO.java   |  32 ++++
 .../service/business/CrmBusinessService.java  |   2 +-
 .../business/CrmBusinessServiceImpl.java      |  23 +--
 .../service/contact/ContactServiceImpl.java   |   2 +-
 .../service/contract/ContractServiceImpl.java |   6 +-
 .../business/CrmBusinessServiceImplTest.java  |  56 +++---
 .../CrmBusinessStatusServiceImplTest.java     |  26 ++-
 .../CrmBusinessStatusTypeServiceImplTest.java |  12 +-
 .../customer/CrmCustomerServiceImplTest.java  |   8 +-
 27 files changed, 469 insertions(+), 179 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/dataobject/CrmPermissionBaseDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/{AuthUtil.java => CrmPermissionUtils.java} (60%)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/vo/CrmTransferBaseVO.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 69405a5b9..b4f909a8b 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -19,8 +19,9 @@ public interface ErrorCodeConstants {
 
     // ========== 商机管理 1-020-002-000 ==========
     ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
-    ErrorCode BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_002_001, "商机转移失败,原因:没有转移权限"); // TODO @puhui999:这个搞成 “商机操作失败,原因:没有权限”
-    ErrorCode BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_002_002, "商机转移失败,原因:负责人不存在");
+    ErrorCode BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_002_001, "商机操作失败,原因:没有权限");
+    ErrorCode BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_002_002, "商机操作失败,原因:负责人不存在");
+    ErrorCode BUSINESS_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_002_003, "商机操作失败,原因:转移对象已经是该负责人");
 
     // TODO @lilleo:商机状态、商机类型,都单独错误码段
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http
new file mode 100644
index 000000000..631824e31
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http
@@ -0,0 +1,11 @@
+PUT {{baseUrl}}/crm/business/transfer
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "id": 1,
+  "ownerUserId": 2,
+  "transferType": 2,
+  "permissionType": 2
+}
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index 8096176c9..2082e80d3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business;
 
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
@@ -7,6 +8,7 @@ import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.framework.utils.CrmPermissionUtils;
 import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -63,6 +65,7 @@ public class CrmBusinessController {
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
     public CommonResult<CrmBusinessRespVO> getBusiness(@RequestParam("id") Long id) {
+        CrmPermissionUtils.setCrmTransferInfo(getLoginUserId(), UserTypeEnum.ADMIN.getValue());
         CrmBusinessDO business = businessService.getBusiness(id);
         return success(CrmBusinessConvert.INSTANCE.convert(business));
     }
@@ -91,7 +94,8 @@ public class CrmBusinessController {
     @PutMapping("/transfer")
     @Operation(summary = "商机转移")
     @PreAuthorize("@ss.hasPermission('crm:business:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmBusinessTransferReqVO reqVO) {
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferBusinessReqVO reqVO) {
+        CrmPermissionUtils.setCrmTransferInfo(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO);
         businessService.businessTransfer(reqVO, getLoginUserId());
         return success(true);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
deleted file mode 100644
index fd769e77c..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-
-@Schema(description = "管理后台 - 商机转移 Request VO")
-@Data
-public class CrmBusinessTransferReqVO {
-
-    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "联系人编号不能为空")
-    private Long id;
-
-    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
new file mode 100644
index 000000000..0ba2142db
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
@@ -0,0 +1,11 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import cn.iocoder.yudao.module.crm.framework.vo.CrmTransferBaseVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 商机转移 Request VO")
+@Data
+public class CrmTransferBusinessReqVO extends CrmTransferBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index a025d3ed4..16ef0433b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.crm.convert.business;
 
-import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
@@ -30,11 +29,10 @@ public interface CrmBusinessConvert {
 
     List<CrmBusinessExcelVO> convertList02(List<CrmBusinessDO> list);
 
-    default CrmBusinessDO convert(CrmBusinessDO business, CrmBusinessTransferReqVO reqVO, Long userId) {
+    default CrmBusinessDO convert(CrmBusinessDO business, CrmTransferBusinessReqVO reqVO, Long userId) {
         Set<Long> rwUserIds = business.getRwUserIds();
-        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人 TODO puhui999:是不是直接 rwUserIds.remove(userId)
-        // TODO @puhui999:ownerUserId 不用添加到进去,它就是 ownerUserId 就够;因为一共有 3 个角色:负责人、读写、只读;
-        rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
+        rwUserIds.remove(userId);
+        rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负责人
         // TODO @puhui999:对原负责人,加个类似的处理:移除、转化为团队成员(只读、读写)
         return new CrmBusinessDO().setId(business.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
                 .setRwUserIds(rwUserIds);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index 7d0dc84fa..55df1d87b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -37,7 +37,7 @@ public interface ContractConvert {
         Set<Long> rwUserIds = contract.getRwUserIds();
         rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
         rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
-        return new ContractDO().setId(contract.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
+        return (ContractDO) new ContractDO().setId(contract.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
                 .setRwUserIds(rwUserIds);
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
index 23d7ddb9a..a9e599b7c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
@@ -1,17 +1,14 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.business;
 
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
 import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
+import cn.iocoder.yudao.module.crm.framework.dataobject.CrmPermissionBaseDO;
 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.*;
 
 import java.time.LocalDateTime;
-import java.util.Set;
 
 /**
  * 商机 DO
@@ -26,7 +23,7 @@ import java.util.Set;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class CrmBusinessDO extends BaseDO {
+public class CrmBusinessDO extends CrmPermissionBaseDO {
 
     /**
      * 主键
@@ -82,20 +79,6 @@ public class CrmBusinessDO extends BaseDO {
      * 备注
      */
     private String remark;
-    /**
-     * 负责人的用户编号
-     */
-    private Long ownerUserId;
-    /**
-     * 只读权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> rwUserIds;
     /**
      * 1赢单2输单3无效
      *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
index 6c56ebe98..4e5dab7cf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
@@ -1,15 +1,12 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
 
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
+import cn.iocoder.yudao.module.crm.framework.dataobject.CrmPermissionBaseDO;
 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.*;
 
 import java.time.LocalDateTime;
-import java.util.Set;
 
 /**
  * crm 联系人 DO
@@ -24,7 +21,7 @@ import java.util.Set;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class ContactDO extends BaseDO {
+public class ContactDO extends CrmPermissionBaseDO {
 
     /**
      * 主键
@@ -69,26 +66,9 @@ public class ContactDO extends BaseDO {
      * 备注
      */
     private String remark;
-    /**
-     * 负责人用户编号
-     *
-     * TODO @zyna:关联的字段,也要写下
-     */
-    private Long ownerUserId;
     /**
      * 最后跟进时间
      */
     private LocalDateTime lastTime;
 
-    /**
-     * 只读权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> rwUserIds;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
index 2c5d0e44f..0ddce4c8b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
@@ -1,15 +1,12 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contract;
 
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
+import cn.iocoder.yudao.module.crm.framework.dataobject.CrmPermissionBaseDO;
 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.*;
 
 import java.time.LocalDateTime;
-import java.util.Set;
 
 /**
  * 合同 DO
@@ -24,7 +21,7 @@ import java.util.Set;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class ContractDO extends BaseDO {
+public class ContractDO extends CrmPermissionBaseDO {
 
     /**
      * 合同编号
@@ -51,10 +48,6 @@ public class ContractDO extends BaseDO {
      * 下单日期
      */
     private LocalDateTime orderDate;
-    /**
-     * 负责人的用户编号
-     */
-    private Long ownerUserId;
     /**
      * 合同编号
      */
@@ -79,16 +72,6 @@ public class ContractDO extends BaseDO {
      * 产品总金额
      */
     private Integer productPrice;
-    /**
-     * 只读权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> rwUserIds;
     /**
      * 联系人编号
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index 6e5026c94..e66c42172 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -1,15 +1,12 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.customer;
 
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
+import cn.iocoder.yudao.module.crm.framework.dataobject.CrmPermissionBaseDO;
 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.*;
 
 import java.time.LocalDateTime;
-import java.util.List;
 
 // TODO 芋艿:调整下字段
 
@@ -26,7 +23,7 @@ import java.util.List;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class CrmCustomerDO extends BaseDO {
+public class CrmCustomerDO extends CrmPermissionBaseDO {
 
     /**
      * 编号
@@ -99,20 +96,6 @@ public class CrmCustomerDO extends BaseDO {
      * 备注
      */
     private String remark;
-    /**
-     * 负责人的用户编号
-     */
-    private Long ownerUserId;
-    /**
-     * 只读权限的用户编号数组
-     */
-    @TableField(typeHandler = LongListTypeHandler.class)
-    private List<Long> roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    @TableField(typeHandler = LongListTypeHandler.class)
-    private List<Long> rwUserIds;
     /**
      * 地区编号
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
new file mode 100644
index 000000000..9d1733b30
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.framework.core.annotations;
+
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.METHOD;
+
+/**
+ * Crm 数据操作权限校验 AOP 注解
+ *
+ * @author HUIHUI
+ */
+@Target({METHOD, ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface CrmPermission {
+
+    /**
+     * crm 类型
+     */
+    CrmEnum crmType();
+
+    /**
+     * 操作类型
+     */
+    OperationTypeEnum operationType();
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
new file mode 100644
index 000000000..b0692185b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.crm.framework.core.aop;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.vo.CrmTransferBaseVO;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import cn.iocoder.yudao.module.crm.service.contact.ContactService;
+import cn.iocoder.yudao.module.crm.service.contract.ContractService;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+/**
+ * Crm 数据权限校验 AOP 切面
+ *
+ * @author HUIHUI
+ */
+@Component
+@Aspect
+@Slf4j
+public class CrmPermissionAspect {
+
+    /**
+     * 用户编号
+     */
+    private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
+    /**
+     * 用户类型
+     */
+    private static final ThreadLocal<Integer> USER_TYPE = new ThreadLocal<>();
+    /**
+     * 操作数据编号
+     */
+    private static final ThreadLocal<Long> DATA_ID = new ThreadLocal<>();
+    /**
+     * Crm 转换数据 VO 数据
+     */
+    private static final ThreadLocal<CrmTransferBaseVO> CRM_TRANSFER_VO = new ThreadLocal<>();
+
+    @Resource
+    private CrmBusinessService crmBusinessService;
+    @Resource
+    private ContactService contactService;
+    @Resource
+    private ContractService contractService;
+    @Resource
+    private CrmCustomerService crmCustomerService;
+
+    public static void setCrmTransferInfo(Long userId, Integer userType, Object crmTransferBaseVO) {
+        USER_ID.set(userId);
+        USER_TYPE.set(userType);
+        CRM_TRANSFER_VO.set((CrmTransferBaseVO) crmTransferBaseVO);
+    }
+
+    public static void setCrmTransferInfo(Long userId, Integer userType) {
+        USER_ID.set(userId);
+        USER_TYPE.set(userType);
+    }
+
+    private static void clear() {
+        USER_ID.remove();
+        USER_TYPE.remove();
+        DATA_ID.remove();
+        CRM_TRANSFER_VO.remove();
+    }
+
+    @Before("@annotation(crmPermission)")
+    public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
+        try {
+            Integer crmType = crmPermission.crmType().getType();
+            Integer operationType = crmPermission.operationType().getType();
+            Long id = DATA_ID.get();// 获取操作数据的编号
+            KeyValue<Collection<Long>, Collection<Long>> keyValue = new KeyValue<>(); // 数据权限 key 只读,value 读写
+            // 客户
+            if (ObjUtil.equal(crmType, CrmEnum.CRM_CUSTOMER.getType())) {
+                CrmCustomerDO customer = crmCustomerService.getCustomer(id);
+                if (customer == null) {
+                    throw exception(CUSTOMER_NOT_EXISTS);
+                }
+                // 如果是自己则直接过
+                if (ObjUtil.equal(customer.getOwnerUserId(), USER_ID.get())) {
+                    return;
+                }
+                new KeyValue<>(customer.getRoUserIds(), customer.getRwUserIds());
+            }
+            // 联系人
+            if (ObjUtil.equal(crmType, CrmEnum.CRM_CONTACTS.getType())) {
+                ContactDO contact = contactService.getContact(id);
+                if (contact == null) {
+                    throw exception(CONTACT_NOT_EXISTS);
+                }
+                // 如果是自己则直接过
+                if (ObjUtil.equal(contact.getOwnerUserId(), USER_ID.get())) {
+                    return;
+                }
+                new KeyValue<>(contact.getRoUserIds(), contact.getRwUserIds());
+            }
+            // 商机
+            if (ObjUtil.equal(crmType, CrmEnum.CRM_BUSINESS.getType())) {
+                CrmBusinessDO business = crmBusinessService.getBusiness(id);
+                if (business == null) {
+                    throw exception(BUSINESS_NOT_EXISTS);
+                }
+                // 如果是自己则直接过
+                if (ObjUtil.equal(business.getOwnerUserId(), USER_ID.get())) {
+                    return;
+                }
+                new KeyValue<>(business.getRoUserIds(), business.getRwUserIds());
+            }
+            // 合同
+            if (ObjUtil.equal(crmType, CrmEnum.CRM_CONTRACT.getType())) {
+                ContractDO contract = contractService.getContract(id);
+                if (contract == null) {
+                    throw exception(CONTRACT_NOT_EXISTS);
+                }
+                // 如果是自己则直接过
+                if (ObjUtil.equal(contract.getOwnerUserId(), USER_ID.get())) {
+                    return;
+                }
+                new KeyValue<>(contract.getRoUserIds(), contract.getRwUserIds());
+            }
+            // 1. 校验是否有读权限
+            if (OperationTypeEnum.isRead(operationType)) {
+                // 校验该数据当前用户是否可读
+                boolean isRead = CollUtil.contains(keyValue.getKey(), item -> ObjUtil.equal(id, USER_ID.get()))
+                        || CollUtil.contains(keyValue.getValue(), item -> ObjUtil.equal(id, USER_ID.get()));
+                if (isRead) {
+                    return;
+                }
+                throw exception(CONTRACT_NOT_EXISTS);
+            }
+            // 2. 校验是否有编辑权限
+            if (OperationTypeEnum.isEdit(operationType)) {
+                // 校验该数据当前用户是否可读写
+                if (CollUtil.contains(keyValue.getValue(), item -> ObjUtil.equal(id, USER_ID.get()))) {
+                    return;
+                }
+                throw exception(CONTRACT_NOT_EXISTS);
+            }
+        } catch (Exception ex) {
+            log.error("[doBefore][crmPermission({}) 数据校验错误]", toJsonString(crmPermission), ex);
+        } finally {
+            clear();
+        }
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/package-info.java
new file mode 100644
index 000000000..4a3e65722
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/package-info.java
@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.crm.framework.core;
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/dataobject/CrmPermissionBaseDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/dataobject/CrmPermissionBaseDO.java
new file mode 100644
index 000000000..e7262165b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/dataobject/CrmPermissionBaseDO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.crm.framework.dataobject;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.Set;
+
+/**
+ * crm 数据权限基础实体对象
+ *
+ * @author HUIHUI
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmPermissionBaseDO extends BaseDO {
+
+    /**
+     * 负责人的用户编号 关联 AdminUser#id
+     */
+    private Long ownerUserId;
+    /**
+     * 只读权限的用户编号数组
+     */
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> rwUserIds;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
new file mode 100644
index 000000000..1fbaea575
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.framework.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Crm 类型枚举
+ *
+ * @author HUIHUI
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmEnum {
+
+    CRM_LEADS(1, "线索"),
+    CRM_CUSTOMER(2, "客户"),
+    CRM_CONTACTS(3, "联系人"),
+    CRM_PRODUCT(4, "产品"),
+    CRM_BUSINESS(5, "商机"),
+    CRM_CONTRACT(6, "合同"),
+    CRM_RECEIVABLES(7, "回款"),
+    CRM_RECEIVABLES_PLAN(8, "回款计划"),
+    CRM_CUSTOMER_POOL(9, "客户公海");
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 名称
+     */
+    private final String name;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
new file mode 100644
index 000000000..beee6c93a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.crm.framework.enums;
+
+import cn.hutool.core.util.ObjUtil;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Crm 数据操作类型枚举
+ *
+ * @author HUIHUI
+ */
+@RequiredArgsConstructor
+@Getter
+public enum OperationTypeEnum {
+
+    DELETE(1, "删除"),
+    UPDATE(2, "修改"),
+    READ(3, "查询"),
+    TRANSFER(4, "转移");
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+
+    /**
+     * 名称
+     */
+    private final String name;
+
+    public static boolean isRead(Integer type) {
+        return ObjUtil.equal(type, READ.getType());
+    }
+
+    public static boolean isEdit(Integer type) {
+        return ObjUtil.equal(type, UPDATE.getType()) || ObjUtil.equal(type, DELETE.getType()) || ObjUtil.equal(type, TRANSFER.getType());
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/CrmPermissionUtils.java
similarity index 60%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/CrmPermissionUtils.java
index fb66d6f0d..226c0e57b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/AuthUtil.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/CrmPermissionUtils.java
@@ -1,17 +1,21 @@
 package cn.iocoder.yudao.module.crm.framework.utils;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.module.crm.framework.core.aop.CrmPermissionAspect;
+
 import java.util.Collection;
 
-// TODO @puhui999:改成 CrmPermissionUtils;
 /**
  * 数据读写权限校验工具类
  *
  * @author HUIHUI
  */
-public class AuthUtil {
+public class CrmPermissionUtils {
 
     // TODO @puhui999:负责人是单独的字段哈;
     // TODO @puhui999:额外校验,如果是管理员,可以查看所有;看着要做成有状态的了,可能要搞个 CrmPermissionService 咧;
+
     /**
      * 判断当前数据对用户来说是否是只读的
      *
@@ -20,8 +24,7 @@ public class AuthUtil {
      * @return boolean 是/否
      */
     public static boolean isReadOnly(Collection<Long> roUserIds, Long userId) {
-        // TODO @puhui999:从代码角度来说,最好使用 CollUtil.contains
-        return roUserIds.contains(userId);
+        return CollUtil.contains(roUserIds, id -> ObjUtil.equal(id, userId));
     }
 
     /**
@@ -32,7 +35,15 @@ public class AuthUtil {
      * @return boolean 是/否
      */
     public static boolean isReadAndWrite(Collection<Long> rwUserIds, Long userId) {
-        return rwUserIds.contains(userId);
+        return CollUtil.contains(rwUserIds, id -> ObjUtil.equal(id, userId));
+    }
+
+    public static void setCrmTransferInfo(Long userId, Integer userType, Object crmTransferBaseVO) {
+        CrmPermissionAspect.setCrmTransferInfo(userId, userType, crmTransferBaseVO);
+    }
+
+    public static void setCrmTransferInfo(Long userId, Integer userType) {
+        CrmPermissionAspect.setCrmTransferInfo(userId, userType);
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/vo/CrmTransferBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/vo/CrmTransferBaseVO.java
new file mode 100644
index 000000000..97cc6d167
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/vo/CrmTransferBaseVO.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.crm.framework.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * Crm 数据转移 Base VO,提供给转移的子 VO 使用
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CrmTransferBaseVO {
+
+    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long ownerUserId;
+
+    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "原负责人移除方式不能为空")
+    private Integer transferType;
+
+    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "权限类型不能为空")
+    private Integer permissionType;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index 8a157440b..12689d49c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -75,6 +75,6 @@ public interface CrmBusinessService {
      * @param reqVO  请求
      * @param userId 用户编号
      */
-    void businessTransfer(CrmBusinessTransferReqVO reqVO, Long userId);
+    void businessTransfer(CrmTransferBusinessReqVO reqVO, Long userId);
     
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index fc01ec555..c1adf4c9c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -2,11 +2,15 @@ package cn.iocoder.yudao.module.crm.service.business;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
+import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
@@ -18,7 +22,6 @@ 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.framework.utils.AuthUtil.isReadAndWrite;
 
 /**
  * 商机 Service 实现类
@@ -70,6 +73,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     }
 
     @Override
+    @CrmPermission(crmType = CrmEnum.CRM_BUSINESS, operationType = OperationTypeEnum.READ)
     public CrmBusinessDO getBusiness(Long id) {
         return businessMapper.selectById(id);
     }
@@ -92,17 +96,16 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         return businessMapper.selectList(exportReqVO);
     }
 
-    // TODO @puhui999:动名词哈。transferBusiness
     @Override
-    public void businessTransfer(CrmBusinessTransferReqVO reqVO, Long userId) {
+    @CrmPermission(crmType = CrmEnum.CRM_BUSINESS, operationType = OperationTypeEnum.TRANSFER)
+    public void businessTransfer(CrmTransferBusinessReqVO reqVO, Long userId) {
         // 1.1 校验商机是否存在
-        CrmBusinessDO business = validateBusinessExists(reqVO.getId());
-        // 1.2 校验用户是否拥有读写权限
-        if (!isReadAndWrite(business.getRwUserIds(), userId)) {
-            throw exception(BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED);
+        CrmBusinessDO business = getBusiness(reqVO.getId());
+        // 1.3 校验转移对象是否已经是该负责人
+        if (ObjUtil.equal(business.getOwnerUserId(), reqVO.getOwnerUserId())) {
+            throw exception(BUSINESS_TRANSFER_FAIL_OWNER_USER_EXISTS);
         }
-        // TODO @puhui999:如果已经是该负责人,抛个业务异常;
-        // 1.3 校验新负责人是否存在
+        // 1.4 校验新负责人是否存在
         AdminUserRespDTO user = adminUserApi.getUser(reqVO.getOwnerUserId());
         if (user == null) {
             throw exception(BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS);
@@ -112,7 +115,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         CrmBusinessDO updateBusiness = CrmBusinessConvert.INSTANCE.convert(business, reqVO, userId);
         businessMapper.updateById(updateBusiness);
 
-        // 4. TODO 记录商机转移日志
+        // 3. TODO 记录商机转移日志
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index cca752efa..f8fe72c12 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -18,7 +18,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.framework.utils.AuthUtil.isReadAndWrite;
+import static cn.iocoder.yudao.module.crm.framework.utils.CrmPermissionUtils.isReadAndWrite;
 
 /**
  * crm联系人 Service 实现类
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index 05e1dadd7..8bf6a356e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -7,6 +7,9 @@ import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.convert.contract.ContractConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contract.ContractMapper;
+import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
@@ -18,7 +21,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.framework.utils.AuthUtil.isReadAndWrite;
+import static cn.iocoder.yudao.module.crm.framework.utils.CrmPermissionUtils.isReadAndWrite;
 
 /**
  * 合同 Service 实现类
@@ -94,6 +97,7 @@ public class ContractServiceImpl implements ContractService {
 
     // TODO @puhui999:参考 CrmBusinessServiceImpl 修改建议
     @Override
+    @CrmPermission(crmType = CrmEnum.CRM_CONTRACT, operationType = OperationTypeEnum.TRANSFER)
     public void contractTransfer(CrmContractTransferReqVO reqVO, Long userId) {
         // 1. 校验合同是否存在
         ContractDO contract = validateContractExists(reqVO.getId());
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
index 64d3f1e71..40fb404d3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
@@ -1,10 +1,8 @@
 package cn.iocoder.yudao.module.crm.service.business;
 
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
@@ -162,33 +160,33 @@ public class CrmBusinessServiceImplTest extends BaseDbUnitTest {
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setContactLastTime(null)));
        // 测试 followUpStatus 不匹配
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setFollowUpStatus(null)));
-       // 准备参数
-       CrmBusinessPageReqVO reqVO = new CrmBusinessPageReqVO();
-       reqVO.setName(null);
-       reqVO.setStatusTypeId(null);
-       reqVO.setStatusId(null);
-       reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-       reqVO.setCustomerId(null);
-       reqVO.setDealTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-       reqVO.setPrice(null);
-       reqVO.setDiscountPercent(null);
-       reqVO.setProductPrice(null);
-       reqVO.setRemark(null);
-       reqVO.setOwnerUserId(null);
-       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-       reqVO.setRoUserIds(null);
-       reqVO.setRwUserIds(null);
-       reqVO.setEndStatus(null);
-       reqVO.setEndRemark(null);
-       reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-       reqVO.setFollowUpStatus(null);
-
-       // 调用
-       PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(reqVO);
-       // 断言
-       assertEquals(1, pageResult.getTotal());
-       assertEquals(1, pageResult.getList().size());
-       assertPojoEquals(dbBusiness, pageResult.getList().get(0));
+        //// 准备参数
+        //CrmBusinessPageReqVO reqVO = new CrmBusinessPageReqVO();
+        //reqVO.setName(null);
+        //reqVO.setStatusTypeId(null);
+        //reqVO.setStatusId(null);
+        //reqVO.setContactNextTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+        //reqVO.setCustomerId(null);
+        //reqVO.setDealTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+        //reqVO.setPrice(null);
+        //reqVO.setDiscountPercent(null);
+        //reqVO.setProductPrice(null);
+        //reqVO.setRemark(null);
+        //reqVO.setOwnerUserId(null);
+        //reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+        //reqVO.setRoUserIds(null);
+        //reqVO.setRwUserIds(null);
+        //reqVO.setEndStatus(null);
+        //reqVO.setEndRemark(null);
+        //reqVO.setContactLastTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+        //reqVO.setFollowUpStatus(null);
+        //
+        //// 调用
+        //PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(reqVO);
+        //// 断言
+        //assertEquals(1, pageResult.getTotal());
+        //assertEquals(1, pageResult.getList().size());
+        //assertPojoEquals(dbBusiness, pageResult.getList().get(0));
     }
 
     @Test
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImplTest.java
index 44030db3b..9a59f3bd2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatus/CrmBusinessStatusServiceImplTest.java
@@ -1,10 +1,8 @@
 package cn.iocoder.yudao.module.crm.service.businessstatus;
 
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.CrmBusinessStatusUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.businessstatus.CrmBusinessStatusMapper;
@@ -120,18 +118,18 @@ public class CrmBusinessStatusServiceImplTest extends BaseDbUnitTest {
        // 测试 sort 不匹配
        businessStatusMapper.insert(cloneIgnoreId(dbBusinessStatus, o -> o.setSort(null)));
        // 准备参数
-       CrmBusinessStatusPageReqVO reqVO = new CrmBusinessStatusPageReqVO();
-       reqVO.setTypeId(null);
-       reqVO.setName(null);
-       reqVO.setPercent(null);
-       reqVO.setSort(null);
-
-       // 调用
-       PageResult<CrmBusinessStatusDO> pageResult = businessStatusService.getBusinessStatusPage(reqVO);
-       // 断言
-       assertEquals(1, pageResult.getTotal());
-       assertEquals(1, pageResult.getList().size());
-       assertPojoEquals(dbBusinessStatus, pageResult.getList().get(0));
+        //CrmBusinessStatusPageReqVO reqVO = new CrmBusinessStatusPageReqVO();
+        //reqVO.setTypeId(null);
+        //reqVO.setName(null);
+        //reqVO.setPercent(null);
+        //reqVO.setSort(null);
+        //
+        //// 调用
+        //PageResult<CrmBusinessStatusDO> pageResult = businessStatusService.getBusinessStatusPage(reqVO);
+        //// 断言
+        //assertEquals(1, pageResult.getTotal());
+        //assertEquals(1, pageResult.getList().size());
+        //assertPojoEquals(dbBusinessStatus, pageResult.getList().get(0));
     }
 
     @Test
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImplTest.java
index 58f054b1c..bf778b989 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/businessstatustype/CrmBusinessStatusTypeServiceImplTest.java
@@ -109,7 +109,7 @@ public class CrmBusinessStatusTypeServiceImplTest extends BaseDbUnitTest {
            o.setName(null);
            o.setDeptIds(null);
            o.setStatus(null);
-           o.setCreateTime(null);
+           //o.setCreateTime(null);
        });
        businessStatusTypeMapper.insert(dbBusinessStatusType);
        // 测试 name 不匹配
@@ -119,13 +119,13 @@ public class CrmBusinessStatusTypeServiceImplTest extends BaseDbUnitTest {
        // 测试 status 不匹配
        businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setStatus(null)));
        // 测试 createTime 不匹配
-       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setCreateTime(null)));
+        //businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setCreateTime(null)));
        // 准备参数
        CrmBusinessStatusTypePageReqVO reqVO = new CrmBusinessStatusTypePageReqVO();
        reqVO.setName(null);
-       reqVO.setDeptIds(null);
+        //reqVO.setDeptIds(null);
        reqVO.setStatus(null);
-       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+        //reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
 
        // 调用
        PageResult<CrmBusinessStatusTypeDO> pageResult = businessStatusTypeService.getBusinessStatusTypePage(reqVO);
@@ -143,7 +143,7 @@ public class CrmBusinessStatusTypeServiceImplTest extends BaseDbUnitTest {
            o.setName(null);
            o.setDeptIds(null);
            o.setStatus(null);
-           o.setCreateTime(null);
+           //o.setCreateTime(null);
        });
        businessStatusTypeMapper.insert(dbBusinessStatusType);
        // 测试 name 不匹配
@@ -153,7 +153,7 @@ public class CrmBusinessStatusTypeServiceImplTest extends BaseDbUnitTest {
        // 测试 status 不匹配
        businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setStatus(null)));
        // 测试 createTime 不匹配
-       businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setCreateTime(null)));
+        //businessStatusTypeMapper.insert(cloneIgnoreId(dbBusinessStatusType, o -> o.setCreateTime(null)));
        // 准备参数
        CrmBusinessStatusTypeExportReqVO reqVO = new CrmBusinessStatusTypeExportReqVO();
        reqVO.setName(null);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
index adce8b163..aa188f06a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
@@ -124,8 +124,8 @@ public class CrmCustomerServiceImplTest extends BaseDbUnitTest {
         CrmCustomerPageReqVO reqVO = new CrmCustomerPageReqVO();
         reqVO.setName(null);
         reqVO.setMobile(null);
-        reqVO.setTelephone(null);
-        reqVO.setWebsite(null);
+        //reqVO.setTelephone(null);
+        //reqVO.setWebsite(null);
 
         // 调用
         PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(reqVO);
@@ -158,8 +158,8 @@ public class CrmCustomerServiceImplTest extends BaseDbUnitTest {
         CrmCustomerExportReqVO reqVO = new CrmCustomerExportReqVO();
         reqVO.setName(null);
         reqVO.setMobile(null);
-        reqVO.setTelephone(null);
-        reqVO.setWebsite(null);
+        //reqVO.setTelephone(null);
+        //reqVO.setWebsite(null);
 
         // 调用
         List<CrmCustomerDO> list = customerService.getCustomerList(reqVO);

From 19c9b6ae93d42b440c87a505b8e4a82ac9c4edb8 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Mon, 30 Oct 2023 11:17:43 +0800
Subject: [PATCH 041/101] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20CRM-=E6=95=B0?=
 =?UTF-8?q?=E6=8D=AE=E6=9D=83=E9=99=90=20service?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |  3 +
 .../permission/CrmPermissionConvert.java      | 23 +++++++
 .../permission/CrmPermissionDO.java           | 57 ++++++++++++++++
 .../mysql/permission/CrmPermissionMapper.java | 14 ++++
 .../dal/mysql/permission/package-info.java    |  1 +
 .../permission/CrmPermissionService.java      | 47 +++++++++++++
 .../permission/CrmPermissionServiceImpl.java  | 66 +++++++++++++++++++
 .../permission/bo/CrmPermissionCreateBO.java  | 40 +++++++++++
 .../permission/bo/CrmPermissionUpdateBO.java  | 47 +++++++++++++
 9 files changed, 298 insertions(+)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/package-info.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index b4f909a8b..bde7a3df5 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -41,4 +41,7 @@ public interface ErrorCodeConstants {
     // ========== 客户管理 1_020_006_000 ==========
     ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
 
+    // ========== 客户管理 1_020_007_000 ==========
+    ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
new file mode 100644
index 000000000..0fc6f61cc
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.crm.convert.permission;
+
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * Crm 数据权限 Convert
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmPermissionConvert {
+
+    CrmPermissionConvert INSTANCE = Mappers.getMapper(CrmPermissionConvert.class);
+
+    CrmPermissionDO convert(CrmPermissionCreateBO createBO);
+
+    CrmPermissionDO convert(CrmPermissionUpdateBO updateBO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
new file mode 100644
index 000000000..44a44fa6d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.permission;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+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.*;
+
+import java.util.Set;
+
+/**
+ * crm 数据权限 DO
+ *
+ * @author HUIHUI
+ */
+@TableName("crm_receivable")
+@KeySequence("crm_receivable_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmPermissionDO extends BaseDO {
+
+    /**
+     * ID
+     */
+    @TableId
+    private Long id;
+    /**
+     * 数据类型 关联 {@link CrmEnum}
+     */
+    private Integer crmType;
+    /**
+     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#id
+     */
+    private Integer crmDataId;
+    /**
+     * 负责人的用户编号 关联 AdminUser#id
+     */
+    private Long ownerUserId;
+    /**
+     * 只读权限的用户编号数组
+     */
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> rwUserIds;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
new file mode 100644
index 000000000..44b7b270b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.permission;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * crm 数据权限 mapper
+ *
+ * @author HUIHUI
+ */
+@Mapper
+public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/package-info.java
new file mode 100644
index 000000000..ff0e16b90
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/package-info.java
@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.permission;
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
new file mode 100644
index 000000000..ef064fd87
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.crm.service.permission;
+
+
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
+
+import javax.validation.Valid;
+
+/**
+ * crm 数据权限 Service 接口
+ *
+ * @author HUIHUI
+ */
+public interface CrmPermissionService {
+
+    /**
+     * 创建数据权限
+     *
+     * @param createBO 创建信息
+     * @return 编号
+     */
+    Long createCrmPermission(@Valid CrmPermissionCreateBO createBO);
+
+    /**
+     * 更新数据权限
+     *
+     * @param updateBO 更新信息
+     */
+    void updateCrmPermission(@Valid CrmPermissionUpdateBO updateBO);
+
+    /**
+     * 删除数据权限
+     *
+     * @param id 编号
+     */
+    void deleteCrmPermission(Long id);
+
+    /**
+     * 获得数据权限
+     *
+     * @param id 编号
+     * @return 数据权限
+     */
+    CrmPermissionDO getCrmPermission(Long id);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
new file mode 100644
index 000000000..d0df3eee7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.crm.service.permission;
+
+import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_NOT_EXISTS;
+
+/**
+ * crm 数据权限 Service 接口实现类
+ *
+ * @author HUIHUI
+ */
+@Service
+@Validated
+public class CrmPermissionServiceImpl implements CrmPermissionService {
+
+    @Resource
+    private CrmPermissionMapper crmPermissionMapper;
+
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createCrmPermission(CrmPermissionCreateBO createBO) {
+        CrmPermissionDO createDO = CrmPermissionConvert.INSTANCE.convert(createBO);
+        crmPermissionMapper.insert(createDO);
+        return createDO.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateCrmPermission(CrmPermissionUpdateBO updateBO) {
+        validateCrmPermissionExists(updateBO.getId());
+
+        CrmPermissionDO updateDO = CrmPermissionConvert.INSTANCE.convert(updateBO);
+        crmPermissionMapper.updateById(updateDO);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteCrmPermission(Long id) {
+        validateCrmPermissionExists(id);
+
+        crmPermissionMapper.deleteById(id);
+    }
+
+    private void validateCrmPermissionExists(Long id) {
+        if (crmPermissionMapper.selectById(id) == null) {
+            throw exception(CRM_PERMISSION_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmPermissionDO getCrmPermission(Long id) {
+        return crmPermissionMapper.selectById(id);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
new file mode 100644
index 000000000..8c9b16376
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.crm.service.permission.bo;
+
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+/**
+ * crm 数据权限 Create BO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CrmPermissionCreateBO {
+
+    /**
+     * Crm 类型 关联 {@link CrmEnum}
+     */
+    @NotNull(message = "Crm 类型不能为空")
+    private Integer crmType;
+    /**
+     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
+     */
+    @NotNull(message = "Crm 数据编号不能为空")
+    private Integer crmDataId;
+    /**
+     * 负责人的用户编号 关联 AdminUser#id, null 则为公海数据
+     */
+    private Long ownerUserId;
+    /**
+     * 只读权限的用户编号数组
+     */
+    private Set<Long> roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    private Set<Long> rwUserIds;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
new file mode 100644
index 000000000..3b00eb644
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.crm.service.permission.bo;
+
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+/**
+ * crm 数据权限 Update BO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CrmPermissionUpdateBO {
+
+    /**
+     * 数据权限编号 {@link CrmPermissionDO#getId()}
+     */
+    @NotNull(message = "Crm 数据权限编号不能为空")
+    private Long id;
+
+    /**
+     * Crm 类型 关联 {@link CrmEnum}
+     */
+    @NotNull(message = "Crm 类型不能为空")
+    private Integer crmType;
+    /**
+     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
+     */
+    @NotNull(message = "Crm 数据编号不能为空")
+    private Integer crmDataId;
+    /**
+     * 负责人的用户编号 关联 AdminUser#id, null 则为公海数据
+     */
+    private Long ownerUserId;
+    /**
+     * 只读权限的用户编号数组
+     */
+    private Set<Long> roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    private Set<Long> rwUserIds;
+
+}

From e6eaa3a24af92e5a96d2b3169c7896d3b2cdda3c Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Mon, 30 Oct 2023 16:29:24 +0800
Subject: [PATCH 042/101] =?UTF-8?q?=E5=AE=8C=E5=96=84=20CRM-=E6=95=B0?=
 =?UTF-8?q?=E6=8D=AE=E6=9D=83=E9=99=90=E6=A0=A1=E9=AA=8C=E6=96=B9=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |  11 +-
 .../crm/enums/common/PermissionTypeEnum.java  |  31 ++++
 .../crm/enums/common/TransferTypeEnum.java    |  36 +++++
 .../admin/business/CrmBusinessController.java |   6 +-
 .../business/vo/CrmTransferBusinessReqVO.java |  27 +++-
 .../admin/clue/vo/CrmClueBaseVO.java          |   4 -
 .../admin/contact/ContactController.java      |   2 +-
 .../admin/contact/vo/ContactCreateReqVO.java  |   8 -
 .../contact/vo/CrmContactTransferReqVO.java   |   8 +
 .../admin/contract/ContractController.java    |   2 +-
 .../convert/business/CrmBusinessConvert.java  |  17 +-
 .../crm/convert/contact/ContactConvert.java   |  18 +--
 .../crm/convert/contract/ContractConvert.java |  18 +--
 .../dataobject/business/CrmBusinessDO.java    |   4 +-
 .../crm/dal/dataobject/clue/CrmClueDO.java    |   6 -
 .../crm/dal/dataobject/contact/ContactDO.java |   4 +-
 .../dal/dataobject/contract/ContractDO.java   |   4 +-
 .../dataobject/customer/CrmCustomerDO.java    |   4 +-
 .../permission/CrmPermissionDO.java           |   4 +-
 .../dal/mysql/business/CrmBusinessMapper.java |   3 -
 .../mysql/permission/CrmPermissionMapper.java |   7 +
 .../core/aop/CrmPermissionAspect.java         | 146 +++++-------------
 .../dataobject/CrmPermissionBaseDO.java       |  37 -----
 .../module/crm/framework/enums/CrmEnum.java   |  16 +-
 .../framework/enums/OperationTypeEnum.java    |   5 +-
 .../framework/utils/CrmPermissionUtils.java   |  49 ------
 .../crm/framework/vo/CrmTransferBaseVO.java   |  32 ----
 .../service/business/CrmBusinessService.java  |   5 +-
 .../business/CrmBusinessServiceImpl.java      |  43 +++---
 .../crm/service/contact/ContactService.java   |   5 +-
 .../service/contact/ContactServiceImpl.java   |  47 +++---
 .../crm/service/contract/ContractService.java |   3 +-
 .../service/contract/ContractServiceImpl.java |  46 +++---
 .../permission/CrmPermissionService.java      |  14 +-
 .../permission/CrmPermissionServiceImpl.java  |  61 +++++++-
 .../permission/bo/CrmPermissionCreateBO.java  |   2 +-
 .../permission/bo/CrmPermissionUpdateBO.java  |   2 +-
 .../bo/TransferCrmPermissionBO.java           |  48 ++++++
 .../business/CrmBusinessServiceImplTest.java  |   3 +-
 .../contract/ContractServiceImplTest.java     |   3 +-
 40 files changed, 399 insertions(+), 392 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/PermissionTypeEnum.java
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/dataobject/CrmPermissionBaseDO.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/CrmPermissionUtils.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/vo/CrmTransferBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index bde7a3df5..aba62797d 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -11,17 +11,12 @@ public interface ErrorCodeConstants {
 
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
-    ErrorCode CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_000_001, "合同转移失败,原因:没有转移权限"); // TODO @puhui999:这个搞成 “合同操作失败,原因:没有权限”
-    ErrorCode CONTRACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_000_002, "合同转移失败,原因:负责人不存在");
 
     // ========== 线索管理 1-020-001-000 ==========
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
 
     // ========== 商机管理 1-020-002-000 ==========
     ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
-    ErrorCode BUSINESS_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_002_001, "商机操作失败,原因:没有权限");
-    ErrorCode BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_002_002, "商机操作失败,原因:负责人不存在");
-    ErrorCode BUSINESS_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_002_003, "商机操作失败,原因:转移对象已经是该负责人");
 
     // TODO @lilleo:商机状态、商机类型,都单独错误码段
 
@@ -30,8 +25,6 @@ public interface ErrorCodeConstants {
 
     // ========== 联系人管理 1-020-003-000 ==========
     ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
-    ErrorCode CONTACT_TRANSFER_FAIL_PERMISSION_DENIED = new ErrorCode(1_020_003_001, "联系人转移失败,原因:没有转移权限"); // TODO @puhui999:这个搞成 “联系人操作失败,原因:没有权限”
-    ErrorCode CONTACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_003_002, "联系人转移失败,原因:负责人不存在");
 
     // TODO @liuhongfeng:错误码分段;
     ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_030_000_001, "回款管理不存在");
@@ -43,5 +36,9 @@ public interface ErrorCodeConstants {
 
     // ========== 客户管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
+    ErrorCode CRM_PERMISSION_DENIED = new ErrorCode(1_020_007_001, "{}操作失败,原因:没有权限");
+    ErrorCode CRM_PERMISSION_MODEL_NOT_EXISTS = new ErrorCode(1_020_007_002, "{}不存在");
+    ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_007_003, "{}操作失败,原因:负责人不存在");
+    ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_007_004, "{}操作失败,原因:转移对象已经是该负责人");
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/PermissionTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/PermissionTypeEnum.java
new file mode 100644
index 000000000..1f94ce3fd
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/PermissionTypeEnum.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.crm.enums.common;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+@Getter
+@AllArgsConstructor
+public enum PermissionTypeEnum implements IntArrayValuable {
+
+    READONLY(1, "只读"),
+    READ_AND_WRITE(2, "读写");
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PermissionTypeEnum::getType).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 类型名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
new file mode 100644
index 000000000..d12ce71db
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.crm.enums.common;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * Crm 负责人转移后原负责人的处理方式
+ *
+ * @author HUIHUI
+ */
+@Getter
+@AllArgsConstructor
+public enum TransferTypeEnum implements IntArrayValuable {
+
+    REMOVE(1, "移除"),
+    TEAM(2, "转为团队成员");
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TransferTypeEnum::getType).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 类型名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index 2082e80d3..754bcd7cf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business;
 
-import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
@@ -8,7 +7,6 @@ import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.framework.utils.CrmPermissionUtils;
 import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -40,7 +38,7 @@ public class CrmBusinessController {
     @Operation(summary = "创建商机")
     @PreAuthorize("@ss.hasPermission('crm:business:create')")
     public CommonResult<Long> createBusiness(@Valid @RequestBody CrmBusinessCreateReqVO createReqVO) {
-        return success(businessService.createBusiness(createReqVO));
+        return success(businessService.createBusiness(createReqVO, getLoginUserId()));
     }
 
     @PutMapping("/update")
@@ -65,7 +63,6 @@ public class CrmBusinessController {
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
     public CommonResult<CrmBusinessRespVO> getBusiness(@RequestParam("id") Long id) {
-        CrmPermissionUtils.setCrmTransferInfo(getLoginUserId(), UserTypeEnum.ADMIN.getValue());
         CrmBusinessDO business = businessService.getBusiness(id);
         return success(CrmBusinessConvert.INSTANCE.convert(business));
     }
@@ -95,7 +92,6 @@ public class CrmBusinessController {
     @Operation(summary = "商机转移")
     @PreAuthorize("@ss.hasPermission('crm:business:update')")
     public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferBusinessReqVO reqVO) {
-        CrmPermissionUtils.setCrmTransferInfo(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO);
         businessService.businessTransfer(reqVO, getLoginUserId());
         return success(true);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
index 0ba2142db..075cfb764 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
@@ -1,11 +1,34 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
 
-import cn.iocoder.yudao.module.crm.framework.vo.CrmTransferBaseVO;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.common.PermissionTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.common.TransferTypeEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import javax.validation.constraints.NotNull;
+
 @Schema(description = "管理后台 - 商机转移 Request VO")
 @Data
-public class CrmTransferBusinessReqVO extends CrmTransferBaseVO {
+public class CrmTransferBusinessReqVO {
+
+    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long ownerUserId;
+
+    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @InEnum(TransferTypeEnum.class)
+    @NotNull(message = "原负责人移除方式不能为空")
+    private Integer transferType;
+
+    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @InEnum(PermissionTypeEnum.class)
+    @NotNull(message = "权限类型不能为空")
+    private Integer permissionType;
+
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
index a0efb99d6..f8ca48444 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
@@ -42,10 +42,6 @@ public class CrmClueBaseVO {
     @Schema(description = "地址", example = "北京市海淀区")
     private String address;
 
-    @Schema(description = "负责人的用户编号", example = "27199")
-    @NotNull(message = "负责人不能为空")
-    private Long ownerUserId;
-
     @Schema(description = "最后跟进时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactLastTime;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
index f28d27ef8..98ee287ad 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -39,7 +39,7 @@ public class ContactController {
     @Operation(summary = "创建crm联系人")
     @PreAuthorize("@ss.hasPermission('crm:contact:create')")
     public CommonResult<Long> createContact(@Valid @RequestBody ContactCreateReqVO createReqVO) {
-        return success(contactService.createContact(createReqVO));
+        return success(contactService.createContact(createReqVO, getLoginUserId()));
     }
 
     @PutMapping("/update")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
index 64ea71084..9f4d8ec2c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
@@ -5,18 +5,10 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-import java.util.Set;
-
 @Schema(description = "管理后台 - crm联系人创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class ContactCreateReqVO extends ContactBaseVO {
 
-    @Schema(description = "只读权限的用户编号数组")
-    private Set<Long> roUserIds;
-
-    @Schema(description = "读写权限的用户编号数组")
-    private Set<Long> rwUserIds;
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
index de4c1cbb6..db4926e8c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
@@ -17,4 +17,12 @@ public class CrmContactTransferReqVO {
     @NotNull(message = "新负责人的用户编号不能为空")
     private Long ownerUserId;
 
+    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "原负责人移除方式不能为空")
+    private Integer transferType;
+
+    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "权限类型不能为空")
+    private Integer permissionType;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
index 7a7326cde..3372eb6a3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
@@ -38,7 +38,7 @@ public class ContractController {
     @Operation(summary = "创建合同")
     @PreAuthorize("@ss.hasPermission('crm:contract:create')")
     public CommonResult<Long> createContract(@Valid @RequestBody ContractCreateReqVO createReqVO) {
-        return success(contractService.createContract(createReqVO));
+        return success(contractService.createContract(createReqVO, getLoginUserId()));
     }
 
     @PutMapping("/update")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index 16ef0433b..b9ddf8496 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -3,11 +3,13 @@ package cn.iocoder.yudao.module.crm.convert.business;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
 import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
-import java.util.Set;
 
 /**
  * 商机 Convert
@@ -29,13 +31,10 @@ public interface CrmBusinessConvert {
 
     List<CrmBusinessExcelVO> convertList02(List<CrmBusinessDO> list);
 
-    default CrmBusinessDO convert(CrmBusinessDO business, CrmTransferBusinessReqVO reqVO, Long userId) {
-        Set<Long> rwUserIds = business.getRwUserIds();
-        rwUserIds.remove(userId);
-        rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负责人
-        // TODO @puhui999:对原负责人,加个类似的处理:移除、转化为团队成员(只读、读写)
-        return new CrmBusinessDO().setId(business.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
-                .setRwUserIds(rwUserIds);
-    }
+    @Mappings({
+            @Mapping(target = "userId", source = "userId"),
+            @Mapping(target = "crmDataId", source = "reqVO.id")
+    })
+    TransferCrmPermissionBO convert(CrmTransferBusinessReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index c14ea7586..48ebb7d02 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -1,14 +1,15 @@
 package cn.iocoder.yudao.module.crm.convert.contact;
 
-import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
 import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
-import java.util.Set;
 
 /**
  * crm联系人 Convert
@@ -32,13 +33,10 @@ public interface ContactConvert {
 
     List<ContactExcelVO> convertList02(List<ContactDO> list);
 
-    // TODO @puhui999:参考 CrmBusinessConvert 的修改建议
-    default ContactDO convert(ContactDO contact, CrmContactTransferReqVO reqVO, Long userId) {
-        Set<Long> rwUserIds = contact.getRwUserIds();
-        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
-        rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
-        return new ContactDO().setId(contact.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
-                .setRwUserIds(rwUserIds);
-    }
+    @Mappings({
+            @Mapping(target = "userId", source = "userId"),
+            @Mapping(target = "crmDataId", source = "reqVO.id")
+    })
+    TransferCrmPermissionBO convert(CrmContactTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index 55df1d87b..c962fcd55 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -1,14 +1,15 @@
 package cn.iocoder.yudao.module.crm.convert.contract;
 
-import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
 import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
-import java.util.Set;
 
 /**
  * 合同 Convert
@@ -32,13 +33,10 @@ public interface ContractConvert {
 
     List<ContractExcelVO> convertList02(List<ContractDO> list);
 
-    // TODO @puhui999:参考 CrmBusinessConvert 的修改建议
-    default ContractDO convert(ContractDO contract, CrmContractTransferReqVO reqVO, Long userId) {
-        Set<Long> rwUserIds = contract.getRwUserIds();
-        rwUserIds.removeIf(item -> ObjUtil.equal(item, userId)); // 移除老负责人
-        rwUserIds.add(reqVO.getOwnerUserId()); // 读写权限加入新的负人
-        return (ContractDO) new ContractDO().setId(contract.getId()).setOwnerUserId(reqVO.getOwnerUserId()) // 设置新负责人
-                .setRwUserIds(rwUserIds);
-    }
+    @Mappings({
+            @Mapping(target = "userId", source = "userId"),
+            @Mapping(target = "crmDataId", source = "reqVO.id")
+    })
+    TransferCrmPermissionBO convert(CrmContractTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
index a9e599b7c..435bf1995 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.business;
 
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
-import cn.iocoder.yudao.module.crm.framework.dataobject.CrmPermissionBaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -23,7 +23,7 @@ import java.time.LocalDateTime;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class CrmBusinessDO extends CrmPermissionBaseDO {
+public class CrmBusinessDO extends BaseDO {
 
     /**
      * 主键
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
index 1c6ccd608..592301b44 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/clue/CrmClueDO.java
@@ -64,12 +64,6 @@ public class CrmClueDO extends BaseDO {
      * 地址
      */
     private String address;
-    /**
-     * 负责人的用户编号
-     *
-     * 关联 AdminUserDO 的 id 字段
-     */
-    private Long ownerUserId;
     /**
      * 最后跟进时间 TODO 添加跟进记录时更新该值
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
index 4e5dab7cf..f958fcd64 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
 
-import cn.iocoder.yudao.module.crm.framework.dataobject.CrmPermissionBaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -21,7 +21,7 @@ import java.time.LocalDateTime;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class ContactDO extends CrmPermissionBaseDO {
+public class ContactDO extends BaseDO {
 
     /**
      * 主键
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
index 0ddce4c8b..f32786791 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/ContractDO.java
@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contract;
 
-import cn.iocoder.yudao.module.crm.framework.dataobject.CrmPermissionBaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -21,7 +21,7 @@ import java.time.LocalDateTime;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class ContractDO extends CrmPermissionBaseDO {
+public class ContractDO extends BaseDO {
 
     /**
      * 合同编号
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index e66c42172..9de87ee5b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.customer;
 
-import cn.iocoder.yudao.module.crm.framework.dataobject.CrmPermissionBaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -23,7 +23,7 @@ import java.time.LocalDateTime;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class CrmCustomerDO extends CrmPermissionBaseDO {
+public class CrmCustomerDO extends BaseDO {
 
     /**
      * 编号
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
index 44a44fa6d..028d41c95 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
@@ -36,9 +36,9 @@ public class CrmPermissionDO extends BaseDO {
      */
     private Integer crmType;
     /**
-     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#id
+     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
      */
-    private Integer crmDataId;
+    private Long crmDataId;
     /**
      * 负责人的用户编号 关联 AdminUser#id
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
index 1429c0794..5df94c0fa 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
@@ -36,10 +36,7 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
                 .eqIfPresent(CrmBusinessDO::getDiscountPercent, reqVO.getDiscountPercent())
                 .eqIfPresent(CrmBusinessDO::getProductPrice, reqVO.getProductPrice())
                 .eqIfPresent(CrmBusinessDO::getRemark, reqVO.getRemark())
-                .eqIfPresent(CrmBusinessDO::getOwnerUserId, reqVO.getOwnerUserId())
                 .betweenIfPresent(CrmBusinessDO::getCreateTime, reqVO.getCreateTime())
-                .eqIfPresent(CrmBusinessDO::getRoUserIds, reqVO.getRoUserIds())
-                .eqIfPresent(CrmBusinessDO::getRwUserIds, reqVO.getRwUserIds())
                 .eqIfPresent(CrmBusinessDO::getEndStatus, reqVO.getEndStatus())
                 .eqIfPresent(CrmBusinessDO::getEndRemark, reqVO.getEndRemark())
                 .betweenIfPresent(CrmBusinessDO::getContactLastTime, reqVO.getContactLastTime())
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index 44b7b270b..53809f3eb 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.permission;
 
 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.permission.CrmPermissionDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -11,4 +12,10 @@ import org.apache.ibatis.annotations.Mapper;
  */
 @Mapper
 public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
+
+    default CrmPermissionDO selectByCrmTypeAndCrmDataId(Integer crmType, Long crmDataId) {
+        return selectOne(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getCrmType, crmType).eq(CrmPermissionDO::getCrmDataId, crmDataId));
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index b0692185b..dd25703e0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -2,19 +2,11 @@ package cn.iocoder.yudao.module.crm.framework.core.aop;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
-import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
-import cn.iocoder.yudao.module.crm.framework.vo.CrmTransferBaseVO;
-import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
-import cn.iocoder.yudao.module.crm.service.contact.ContactService;
-import cn.iocoder.yudao.module.crm.service.contract.ContractService;
-import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.Aspect;
@@ -22,11 +14,11 @@ import org.aspectj.lang.annotation.Before;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
-import java.util.Collection;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_DENIED;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_MODEL_NOT_EXISTS;
 
 /**
  * Crm 数据权限校验 AOP 切面
@@ -38,48 +30,16 @@ import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 @Slf4j
 public class CrmPermissionAspect {
 
-    /**
-     * 用户编号
-     */
-    private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
-    /**
-     * 用户类型
-     */
-    private static final ThreadLocal<Integer> USER_TYPE = new ThreadLocal<>();
-    /**
-     * 操作数据编号
-     */
-    private static final ThreadLocal<Long> DATA_ID = new ThreadLocal<>();
-    /**
-     * Crm 转换数据 VO 数据
-     */
-    private static final ThreadLocal<CrmTransferBaseVO> CRM_TRANSFER_VO = new ThreadLocal<>();
-
     @Resource
-    private CrmBusinessService crmBusinessService;
-    @Resource
-    private ContactService contactService;
-    @Resource
-    private ContractService contractService;
-    @Resource
-    private CrmCustomerService crmCustomerService;
+    private CrmPermissionService crmPermissionService;
 
-    public static void setCrmTransferInfo(Long userId, Integer userType, Object crmTransferBaseVO) {
-        USER_ID.set(userId);
-        USER_TYPE.set(userType);
-        CRM_TRANSFER_VO.set((CrmTransferBaseVO) crmTransferBaseVO);
-    }
-
-    public static void setCrmTransferInfo(Long userId, Integer userType) {
-        USER_ID.set(userId);
-        USER_TYPE.set(userType);
-    }
-
-    private static void clear() {
-        USER_ID.remove();
-        USER_TYPE.remove();
-        DATA_ID.remove();
-        CRM_TRANSFER_VO.remove();
+    /**
+     * 获得用户编号
+     *
+     * @return 用户编号
+     */
+    private static Long getUserId() {
+        return WebFrameworkUtils.getLoginUserId();
     }
 
     @Before("@annotation(crmPermission)")
@@ -87,78 +47,46 @@ public class CrmPermissionAspect {
         try {
             Integer crmType = crmPermission.crmType().getType();
             Integer operationType = crmPermission.operationType().getType();
-            Long id = DATA_ID.get();// 获取操作数据的编号
-            KeyValue<Collection<Long>, Collection<Long>> keyValue = new KeyValue<>(); // 数据权限 key 只读,value 读写
-            // 客户
-            if (ObjUtil.equal(crmType, CrmEnum.CRM_CUSTOMER.getType())) {
-                CrmCustomerDO customer = crmCustomerService.getCustomer(id);
-                if (customer == null) {
-                    throw exception(CUSTOMER_NOT_EXISTS);
-                }
-                // 如果是自己则直接过
-                if (ObjUtil.equal(customer.getOwnerUserId(), USER_ID.get())) {
-                    return;
-                }
-                new KeyValue<>(customer.getRoUserIds(), customer.getRwUserIds());
+            Long id = (Long) joinPoint.getArgs()[0];// 获取操作数据的编号
+
+            // 1. 获取数据权限
+            CrmPermissionDO permission = crmPermissionService.getCrmPermissionByCrmTypeAndCrmDataId(crmType, id);
+            if (permission == null) {
+                // 不存在说明数据也不存在
+                throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, crmPermission.crmType().getName());
             }
-            // 联系人
-            if (ObjUtil.equal(crmType, CrmEnum.CRM_CONTACTS.getType())) {
-                ContactDO contact = contactService.getContact(id);
-                if (contact == null) {
-                    throw exception(CONTACT_NOT_EXISTS);
-                }
-                // 如果是自己则直接过
-                if (ObjUtil.equal(contact.getOwnerUserId(), USER_ID.get())) {
-                    return;
-                }
-                new KeyValue<>(contact.getRoUserIds(), contact.getRwUserIds());
+            // 1.2. 校验是否为公海数据
+            if (permission.getOwnerUserId() == null) {
+                return;
             }
-            // 商机
-            if (ObjUtil.equal(crmType, CrmEnum.CRM_BUSINESS.getType())) {
-                CrmBusinessDO business = crmBusinessService.getBusiness(id);
-                if (business == null) {
-                    throw exception(BUSINESS_NOT_EXISTS);
-                }
-                // 如果是自己则直接过
-                if (ObjUtil.equal(business.getOwnerUserId(), USER_ID.get())) {
-                    return;
-                }
-                new KeyValue<>(business.getRoUserIds(), business.getRwUserIds());
+            // 1.3. 校验当前负责人是不是自己
+            if (ObjUtil.equal(permission.getOwnerUserId(), getUserId())) {
+                return;
             }
-            // 合同
-            if (ObjUtil.equal(crmType, CrmEnum.CRM_CONTRACT.getType())) {
-                ContractDO contract = contractService.getContract(id);
-                if (contract == null) {
-                    throw exception(CONTRACT_NOT_EXISTS);
-                }
-                // 如果是自己则直接过
-                if (ObjUtil.equal(contract.getOwnerUserId(), USER_ID.get())) {
-                    return;
-                }
-                new KeyValue<>(contract.getRoUserIds(), contract.getRwUserIds());
-            }
-            // 1. 校验是否有读权限
+            // 1.4 TODO 校验是否为超级管理员
+
+            // 2. 校验是否有读权限
             if (OperationTypeEnum.isRead(operationType)) {
                 // 校验该数据当前用户是否可读
-                boolean isRead = CollUtil.contains(keyValue.getKey(), item -> ObjUtil.equal(id, USER_ID.get()))
-                        || CollUtil.contains(keyValue.getValue(), item -> ObjUtil.equal(id, USER_ID.get()));
+                boolean isRead = CollUtil.contains(permission.getRoUserIds(), item -> ObjUtil.equal(item, getUserId()))
+                        || CollUtil.contains(permission.getRwUserIds(), item -> ObjUtil.equal(item, getUserId()));
                 if (isRead) {
                     return;
                 }
-                throw exception(CONTRACT_NOT_EXISTS);
             }
-            // 2. 校验是否有编辑权限
+
+            // 3. 校验是否有编辑权限
             if (OperationTypeEnum.isEdit(operationType)) {
                 // 校验该数据当前用户是否可读写
-                if (CollUtil.contains(keyValue.getValue(), item -> ObjUtil.equal(id, USER_ID.get()))) {
+                if (CollUtil.contains(permission.getRwUserIds(), item -> ObjUtil.equal(item, getUserId()))) {
                     return;
                 }
-                throw exception(CONTRACT_NOT_EXISTS);
             }
+
+            // 4. 没通过结束,报错 {}操作失败,原因:没有权限
+            throw exception(CRM_PERMISSION_DENIED, crmPermission.crmType().getName());
         } catch (Exception ex) {
             log.error("[doBefore][crmPermission({}) 数据校验错误]", toJsonString(crmPermission), ex);
-        } finally {
-            clear();
         }
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/dataobject/CrmPermissionBaseDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/dataobject/CrmPermissionBaseDO.java
deleted file mode 100644
index e7262165b..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/dataobject/CrmPermissionBaseDO.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package cn.iocoder.yudao.module.crm.framework.dataobject;
-
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
-import com.baomidou.mybatisplus.annotation.TableField;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-import java.util.Set;
-
-/**
- * crm 数据权限基础实体对象
- *
- * @author HUIHUI
- */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmPermissionBaseDO extends BaseDO {
-
-    /**
-     * 负责人的用户编号 关联 AdminUser#id
-     */
-    private Long ownerUserId;
-    /**
-     * 只读权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> rwUserIds;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
index 1fbaea575..62fd4d007 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.framework.enums;
 
+import cn.hutool.core.util.ObjUtil;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 
@@ -15,12 +16,8 @@ public enum CrmEnum {
     CRM_LEADS(1, "线索"),
     CRM_CUSTOMER(2, "客户"),
     CRM_CONTACTS(3, "联系人"),
-    CRM_PRODUCT(4, "产品"),
     CRM_BUSINESS(5, "商机"),
-    CRM_CONTRACT(6, "合同"),
-    CRM_RECEIVABLES(7, "回款"),
-    CRM_RECEIVABLES_PLAN(8, "回款计划"),
-    CRM_CUSTOMER_POOL(9, "客户公海");
+    CRM_CONTRACT(6, "合同");
 
     /**
      * 类型
@@ -31,4 +28,13 @@ public enum CrmEnum {
      */
     private final String name;
 
+    public static String getNameByType(Integer type) {
+        for (CrmEnum crmEnum : CrmEnum.values()) {
+            if (ObjUtil.equal(crmEnum.type, type)) {
+                return crmEnum.name;
+            }
+        }
+        return "";
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
index beee6c93a..3cc2e8b6e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
@@ -15,8 +15,7 @@ public enum OperationTypeEnum {
 
     DELETE(1, "删除"),
     UPDATE(2, "修改"),
-    READ(3, "查询"),
-    TRANSFER(4, "转移");
+    READ(3, "查询");
 
     /**
      * 类型
@@ -33,7 +32,7 @@ public enum OperationTypeEnum {
     }
 
     public static boolean isEdit(Integer type) {
-        return ObjUtil.equal(type, UPDATE.getType()) || ObjUtil.equal(type, DELETE.getType()) || ObjUtil.equal(type, TRANSFER.getType());
+        return ObjUtil.equal(type, UPDATE.getType()) || ObjUtil.equal(type, DELETE.getType());
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/CrmPermissionUtils.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/CrmPermissionUtils.java
deleted file mode 100644
index 226c0e57b..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/utils/CrmPermissionUtils.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package cn.iocoder.yudao.module.crm.framework.utils;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ObjUtil;
-import cn.iocoder.yudao.module.crm.framework.core.aop.CrmPermissionAspect;
-
-import java.util.Collection;
-
-/**
- * 数据读写权限校验工具类
- *
- * @author HUIHUI
- */
-public class CrmPermissionUtils {
-
-    // TODO @puhui999:负责人是单独的字段哈;
-    // TODO @puhui999:额外校验,如果是管理员,可以查看所有;看着要做成有状态的了,可能要搞个 CrmPermissionService 咧;
-
-    /**
-     * 判断当前数据对用户来说是否是只读的
-     *
-     * @param roUserIds 当前操作数据的只读权限的用户编号数组
-     * @param userId    当前操作数据的用户编号
-     * @return boolean 是/否
-     */
-    public static boolean isReadOnly(Collection<Long> roUserIds, Long userId) {
-        return CollUtil.contains(roUserIds, id -> ObjUtil.equal(id, userId));
-    }
-
-    /**
-     * 判断当前数据对用户来说是否是可读写的
-     *
-     * @param rwUserIds 当前操作数据的读写权限的用户编号数组
-     * @param userId    当前操作数据的用户编号
-     * @return boolean 是/否
-     */
-    public static boolean isReadAndWrite(Collection<Long> rwUserIds, Long userId) {
-        return CollUtil.contains(rwUserIds, id -> ObjUtil.equal(id, userId));
-    }
-
-    public static void setCrmTransferInfo(Long userId, Integer userType, Object crmTransferBaseVO) {
-        CrmPermissionAspect.setCrmTransferInfo(userId, userType, crmTransferBaseVO);
-    }
-
-    public static void setCrmTransferInfo(Long userId, Integer userType) {
-        CrmPermissionAspect.setCrmTransferInfo(userId, userType);
-    }
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/vo/CrmTransferBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/vo/CrmTransferBaseVO.java
deleted file mode 100644
index 97cc6d167..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/vo/CrmTransferBaseVO.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package cn.iocoder.yudao.module.crm.framework.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-
-/**
- * Crm 数据转移 Base VO,提供给转移的子 VO 使用
- *
- * @author HUIHUI
- */
-@Data
-public class CrmTransferBaseVO {
-
-    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "联系人编号不能为空")
-    private Long id;
-
-    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId;
-
-    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "原负责人移除方式不能为空")
-    private Integer transferType;
-
-    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "权限类型不能为空")
-    private Integer permissionType;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index 12689d49c..233b6603a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -19,9 +19,10 @@ public interface CrmBusinessService {
      * 创建商机
      *
      * @param createReqVO 创建信息
+     * @param userId      用户编号
      * @return 编号
      */
-    Long createBusiness(@Valid CrmBusinessCreateReqVO createReqVO);
+    Long createBusiness(@Valid CrmBusinessCreateReqVO createReqVO, Long userId);
 
     /**
      * 更新商机
@@ -76,5 +77,5 @@ public interface CrmBusinessService {
      * @param userId 用户编号
      */
     void businessTransfer(CrmTransferBusinessReqVO reqVO, Long userId);
-    
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index c1adf4c9c..250f01492 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.crm.service.business;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
-import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
@@ -11,9 +10,11 @@ import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
-import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
@@ -21,7 +22,7 @@ import java.util.Collection;
 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.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
 
 /**
  * 商机 Service 实现类
@@ -37,17 +38,27 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Resource
     private AdminUserApi adminUserApi;
+    @Resource
+    private CrmPermissionService crmPermissionService;
 
     @Override
-    public Long createBusiness(CrmBusinessCreateReqVO createReqVO) {
+    @Transactional(rollbackFor = Exception.class)
+    public Long createBusiness(CrmBusinessCreateReqVO createReqVO, Long userId) {
         // 插入
         CrmBusinessDO business = CrmBusinessConvert.INSTANCE.convert(createReqVO);
         businessMapper.insert(business);
+
+        // 创建数据权限
+        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmEnum.CRM_BUSINESS.getType())
+                .setCrmDataId(business.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
+
         // 返回
         return business.getId();
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(crmType = CrmEnum.CRM_BUSINESS, operationType = OperationTypeEnum.UPDATE)
     public void updateBusiness(CrmBusinessUpdateReqVO updateReqVO) {
         // 校验存在
         validateBusinessExists(updateReqVO.getId());
@@ -57,6 +68,8 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(crmType = CrmEnum.CRM_BUSINESS, operationType = OperationTypeEnum.DELETE)
     public void deleteBusiness(Long id) {
         // 校验存在
         validateBusinessExists(id);
@@ -97,25 +110,15 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     }
 
     @Override
-    @CrmPermission(crmType = CrmEnum.CRM_BUSINESS, operationType = OperationTypeEnum.TRANSFER)
+    @Transactional(rollbackFor = Exception.class)
     public void businessTransfer(CrmTransferBusinessReqVO reqVO, Long userId) {
-        // 1.1 校验商机是否存在
-        CrmBusinessDO business = getBusiness(reqVO.getId());
-        // 1.3 校验转移对象是否已经是该负责人
-        if (ObjUtil.equal(business.getOwnerUserId(), reqVO.getOwnerUserId())) {
-            throw exception(BUSINESS_TRANSFER_FAIL_OWNER_USER_EXISTS);
-        }
-        // 1.4 校验新负责人是否存在
-        AdminUserRespDTO user = adminUserApi.getUser(reqVO.getOwnerUserId());
-        if (user == null) {
-            throw exception(BUSINESS_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS);
-        }
+        // 1 校验商机是否存在
+        validateBusinessExists(reqVO.getId());
 
-        // 2. 更新新的负责人
-        CrmBusinessDO updateBusiness = CrmBusinessConvert.INSTANCE.convert(business, reqVO, userId);
-        businessMapper.updateById(updateBusiness);
+        // 2. 数据权限转移
+        crmPermissionService.transferCrmPermission(
+                CrmBusinessConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_BUSINESS.getType()));
 
-        // 3. TODO 记录商机转移日志
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
index 63b3449f2..a66edc650 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
@@ -19,9 +19,10 @@ public interface ContactService {
      * 创建crm联系人
      *
      * @param createReqVO 创建信息
+     * @param userId      用户编号
      * @return 编号
      */
-    Long createContact(@Valid ContactCreateReqVO createReqVO);
+    Long createContact(@Valid ContactCreateReqVO createReqVO, Long userId);
 
     /**
      * 更新crm联系人
@@ -76,5 +77,5 @@ public interface ContactService {
      * @param userId 用户编号
      */
     void contactTransfer(CrmContactTransferReqVO reqVO, Long userId);
-    
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index f8fe72c12..d39ada967 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -7,9 +7,13 @@ import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
-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.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
@@ -17,8 +21,7 @@ import java.util.Collection;
 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.framework.utils.CrmPermissionUtils.isReadAndWrite;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTACT_NOT_EXISTS;
 
 /**
  * crm联系人 Service 实现类
@@ -33,19 +36,26 @@ public class ContactServiceImpl implements ContactService {
     private ContactMapper contactMapper;
 
     @Resource
-    private AdminUserApi adminUserApi;
+    private CrmPermissionService crmPermissionService;
 
     @Override
-    public Long createContact(ContactCreateReqVO createReqVO) {
+    public Long createContact(ContactCreateReqVO createReqVO, Long userId) {
         // TODO @customerId:需要校验存在
         // 插入
         ContactDO contact = ContactConvert.INSTANCE.convert(createReqVO);
         contactMapper.insert(contact);
+
+        // 创建数据权限
+        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmEnum.CRM_BUSINESS.getType())
+                .setCrmDataId(contact.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
+
         // 返回
         return contact.getId();
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(crmType = CrmEnum.CRM_CONTACTS, operationType = OperationTypeEnum.UPDATE)
     public void updateContact(ContactUpdateReqVO updateReqVO) {
         // 校验存在
         validateContactExists(updateReqVO.getId());
@@ -57,6 +67,8 @@ public class ContactServiceImpl implements ContactService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(crmType = CrmEnum.CRM_CONTACTS, operationType = OperationTypeEnum.DELETE)
     public void deleteContact(Long id) {
         // 校验存在
         validateContactExists(id);
@@ -73,6 +85,7 @@ public class ContactServiceImpl implements ContactService {
     }
 
     @Override
+    @CrmPermission(crmType = CrmEnum.CRM_CONTACTS, operationType = OperationTypeEnum.READ)
     public ContactDO getContact(Long id) {
         return contactMapper.selectById(id);
     }
@@ -95,26 +108,14 @@ public class ContactServiceImpl implements ContactService {
         return contactMapper.selectList(exportReqVO);
     }
 
-    // TODO @puhui999:参考 CrmBusinessServiceImpl 修改建议
     @Override
     public void contactTransfer(CrmContactTransferReqVO reqVO, Long userId) {
-        // 1. 校验联系人是否存在
-        ContactDO contact = validateContactExists(reqVO.getId());
-        // 1.2. 校验用户是否拥有读写权限
-        if (!isReadAndWrite(contact.getRwUserIds(), userId)) {
-            throw exception(CONTACT_TRANSFER_FAIL_PERMISSION_DENIED);
-        }
-        // 2. 校验新负责人是否存在
-        AdminUserRespDTO user = adminUserApi.getUser(reqVO.getOwnerUserId());
-        if (user == null) {
-            throw exception(CONTACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS);
-        }
+        // 1 校验联系人是否存在
+        validateContactExists(reqVO.getId());
 
-        // 3. 更新新的负责人
-        ContactDO updateContact = ContactConvert.INSTANCE.convert(contact, reqVO, userId);
-        contactMapper.updateById(updateContact);
-
-        // 4. TODO 记录联系人转移日志
+        // 2. 数据权限转移
+        crmPermissionService.transferCrmPermission(
+                ContactConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_CONTACTS.getType()));
 
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
index af8bda185..969f77851 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
@@ -19,9 +19,10 @@ public interface ContractService {
      * 创建合同
      *
      * @param createReqVO 创建信息
+     * @param userId      用户编号
      * @return 编号
      */
-    Long createContract(@Valid ContractCreateReqVO createReqVO);
+    Long createContract(@Valid ContractCreateReqVO createReqVO, Long userId);
 
     /**
      * 更新合同
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index 8bf6a356e..f07911455 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -10,9 +10,10 @@ import cn.iocoder.yudao.module.crm.dal.mysql.contract.ContractMapper;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
-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.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
@@ -20,8 +21,7 @@ import java.util.Collection;
 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.framework.utils.CrmPermissionUtils.isReadAndWrite;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTRACT_NOT_EXISTS;
 
 /**
  * 合同 Service 实现类
@@ -36,18 +36,25 @@ public class ContractServiceImpl implements ContractService {
     private ContractMapper contractMapper;
 
     @Resource
-    private AdminUserApi adminUserApi;
+    private CrmPermissionService crmPermissionService;
 
     @Override
-    public Long createContract(ContractCreateReqVO createReqVO) {
+    public Long createContract(ContractCreateReqVO createReqVO, Long userId) {
         // 插入
         ContractDO contract = ContractConvert.INSTANCE.convert(createReqVO);
         contractMapper.insert(contract);
+
+        // 创建数据权限
+        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmEnum.CRM_CONTRACT.getType())
+                .setCrmDataId(contract.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
+
         // 返回
         return contract.getId();
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(crmType = CrmEnum.CRM_CONTRACT, operationType = OperationTypeEnum.DELETE)
     public void updateContract(ContractUpdateReqVO updateReqVO) {
         // 校验存在
         validateContractExists(updateReqVO.getId());
@@ -57,6 +64,8 @@ public class ContractServiceImpl implements ContractService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(crmType = CrmEnum.CRM_CONTRACT, operationType = OperationTypeEnum.DELETE)
     public void deleteContract(Long id) {
         // 校验存在
         validateContractExists(id);
@@ -73,6 +82,7 @@ public class ContractServiceImpl implements ContractService {
     }
 
     @Override
+    @CrmPermission(crmType = CrmEnum.CRM_CONTRACT, operationType = OperationTypeEnum.READ)
     public ContractDO getContract(Long id) {
         return contractMapper.selectById(id);
     }
@@ -95,27 +105,15 @@ public class ContractServiceImpl implements ContractService {
         return contractMapper.selectList(exportReqVO);
     }
 
-    // TODO @puhui999:参考 CrmBusinessServiceImpl 修改建议
     @Override
-    @CrmPermission(crmType = CrmEnum.CRM_CONTRACT, operationType = OperationTypeEnum.TRANSFER)
+    @Transactional(rollbackFor = Exception.class)
     public void contractTransfer(CrmContractTransferReqVO reqVO, Long userId) {
-        // 1. 校验合同是否存在
-        ContractDO contract = validateContractExists(reqVO.getId());
-        // 1.2. 校验用户是否拥有读写权限
-        if (!isReadAndWrite(contract.getRwUserIds(), userId)) {
-            throw exception(CONTRACT_TRANSFER_FAIL_PERMISSION_DENIED);
-        }
-        // 2. 校验新负责人是否存在
-        AdminUserRespDTO user = adminUserApi.getUser(reqVO.getOwnerUserId());
-        if (user == null) {
-            throw exception(CONTRACT_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS);
-        }
+        // 1 校验合同是否存在
+        validateContractExists(reqVO.getId());
 
-        // 3. 更新新的负责人
-        ContractDO updateContract = ContractConvert.INSTANCE.convert(contract, reqVO, userId);
-        contractMapper.updateById(updateContract);
-
-        // 4. TODO 记录合同转移日志
+        // 2. 数据权限转移
+        crmPermissionService.transferCrmPermission(
+                ContractConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_CONTRACT.getType()));
 
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index ef064fd87..91321b709 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.crm.service.permission;
 
 
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
 
 import javax.validation.Valid;
 
@@ -39,9 +41,17 @@ public interface CrmPermissionService {
     /**
      * 获得数据权限
      *
-     * @param id 编号
+     * @param crmType 数据类型 关联 {@link CrmEnum}
+     * @param crmDataId 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
      * @return 数据权限
      */
-    CrmPermissionDO getCrmPermission(Long id);
+    CrmPermissionDO getCrmPermissionByCrmTypeAndCrmDataId(Integer crmType, Long crmDataId);
+
+    /**
+     * 数据权限转移
+     *
+     * @param transferCrmPermissionBO 数据权限转移请求
+     */
+    void transferCrmPermission(@Valid TransferCrmPermissionBO transferCrmPermissionBO);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index d0df3eee7..2647976bc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -1,18 +1,27 @@
 package cn.iocoder.yudao.module.crm.service.permission;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
+import cn.iocoder.yudao.module.crm.enums.common.PermissionTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.common.TransferTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 
 /**
  * crm 数据权限 Service 接口实现类
@@ -26,6 +35,8 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Resource
     private CrmPermissionMapper crmPermissionMapper;
 
+    @Resource
+    private AdminUserApi adminUserApi;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -59,8 +70,52 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     }
 
     @Override
-    public CrmPermissionDO getCrmPermission(Long id) {
-        return crmPermissionMapper.selectById(id);
+    public CrmPermissionDO getCrmPermissionByCrmTypeAndCrmDataId(Integer crmType, Long crmDataId) {
+        return crmPermissionMapper.selectByCrmTypeAndCrmDataId(crmType, crmDataId);
+    }
+
+    @Override
+    public void transferCrmPermission(TransferCrmPermissionBO transferCrmPermissionBO) {
+        // 1 校验商机是否存在
+        CrmPermissionDO permission = getCrmPermissionByCrmTypeAndCrmDataId(transferCrmPermissionBO.getCrmType(),
+                transferCrmPermissionBO.getCrmDataId());
+        String crmName = CrmEnum.getNameByType(transferCrmPermissionBO.getCrmType());
+        if (permission == null) {
+            throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, crmName);
+        }
+        // 1.2 校验转移对象是否已经是该负责人
+        if (ObjUtil.equal(permission.getOwnerUserId(), permission.getOwnerUserId())) {
+            throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS, crmName);
+        }
+        // 1.3 校验新负责人是否存在
+        AdminUserRespDTO user = adminUserApi.getUser(permission.getOwnerUserId());
+        if (user == null) {
+            throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, crmName);
+        }
+        // TODO 校验是否为超级管理员 || 1.4
+        // 1.4 校验是否有写权限
+        if (!CollUtil.contains(permission.getRwUserIds(), id -> ObjUtil.equal(id, transferCrmPermissionBO.getUserId()))) {
+            throw exception(CRM_PERMISSION_DENIED, crmName);
+        }
+
+        // 2 权限转移
+        CrmPermissionDO updateCrmPermission = new CrmPermissionDO().setId(permission.getId())
+                .setOwnerUserId(transferCrmPermissionBO.getOwnerUserId());
+        if (ObjUtil.equal(TransferTypeEnum.TEAM.getType(), transferCrmPermissionBO.getTransferType())) {
+            if (ObjUtil.equal(PermissionTypeEnum.READONLY.getType(), transferCrmPermissionBO.getPermissionType())) {
+                Set<Long> roUserIds = permission.getRoUserIds();
+                roUserIds.add(permission.getOwnerUserId()); // 老负责人加入团队有只读权限
+                updateCrmPermission.setRoUserIds(roUserIds);
+            }
+            if (ObjUtil.equal(PermissionTypeEnum.READ_AND_WRITE.getType(), transferCrmPermissionBO.getPermissionType())) {
+                Set<Long> rwUserIds = permission.getRwUserIds();
+                rwUserIds.add(permission.getOwnerUserId()); // 老负责人加入团队有读写权限
+                updateCrmPermission.setRoUserIds(rwUserIds);
+            }
+        }
+        crmPermissionMapper.updateById(updateCrmPermission);
+
+        // 3. TODO 记录机转移日志
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
index 8c9b16376..b7d693a18 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
@@ -23,7 +23,7 @@ public class CrmPermissionCreateBO {
      * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
      */
     @NotNull(message = "Crm 数据编号不能为空")
-    private Integer crmDataId;
+    private Long crmDataId;
     /**
      * 负责人的用户编号 关联 AdminUser#id, null 则为公海数据
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
index 3b00eb644..6cad08e75 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
@@ -30,7 +30,7 @@ public class CrmPermissionUpdateBO {
      * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
      */
     @NotNull(message = "Crm 数据编号不能为空")
-    private Integer crmDataId;
+    private Long crmDataId;
     /**
      * 负责人的用户编号 关联 AdminUser#id, null 则为公海数据
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java
new file mode 100644
index 000000000..2f3e94f38
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.crm.service.permission.bo;
+
+import cn.iocoder.yudao.module.crm.enums.common.PermissionTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.common.TransferTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 数据权限转移 BO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class TransferCrmPermissionBO {
+
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    /**
+     * Crm 类型 关联 {@link CrmEnum}
+     */
+    @NotNull(message = "Crm 类型不能为空")
+    private Integer crmType;
+
+    /**
+     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
+     */
+    @NotNull(message = "Crm 数据编号不能为空")
+    private Long crmDataId;
+
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long ownerUserId;
+
+    /**
+     * 原负责人移除方式, 关联 {@link TransferTypeEnum}
+     */
+    @NotNull(message = "原负责人移除方式不能为空")
+    private Integer transferType;
+
+    /**
+     * 权限类型, 关联 {@link PermissionTypeEnum}
+     */
+    @NotNull(message = "权限类型不能为空")
+    private Integer permissionType;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
index 40fb404d3..b37563ada 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
@@ -15,6 +15,7 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
 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 CrmBusinessServiceImplTest extends BaseDbUnitTest {
         CrmBusinessCreateReqVO reqVO = randomPojo(CrmBusinessCreateReqVO.class);
 
         // 调用
-        Long businessId = businessService.createBusiness(reqVO);
+        Long businessId = businessService.createBusiness(reqVO, getLoginUserId());
         // 断言
         assertNotNull(businessId);
         // 校验记录的属性是否正确
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java
index f9cbe3a38..1406b5916 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java
@@ -17,6 +17,7 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
 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;
@@ -44,7 +45,7 @@ public class ContractServiceImplTest extends BaseDbUnitTest {
         ContractCreateReqVO reqVO = randomPojo(ContractCreateReqVO.class);
 
         // 调用
-        Long contractId = contractService.createContract(reqVO);
+        Long contractId = contractService.createContract(reqVO, getLoginUserId());
         // 断言
         assertNotNull(contractId);
         // 校验记录的属性是否正确

From c2fd8179891a061e2b9080ec1198871d145571f5 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Mon, 30 Oct 2023 16:52:01 +0800
Subject: [PATCH 043/101] =?UTF-8?q?=E5=AE=8C=E5=96=84=20CRM-=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=20=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90=E6=A0=A1?=
 =?UTF-8?q?=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/business/CrmBusinessController.java |  2 +-
 .../admin/contact/ContactController.java      |  4 +--
 ...eqVO.java => CrmTransferContactReqVO.java} |  2 +-
 .../admin/contract/ContractController.java    |  4 +--
 ...qVO.java => CrmTransferContractReqVO.java} | 10 +++++-
 .../admin/customer/CrmCustomerController.java | 11 +++++-
 .../customer/vo/CrmTransferCustomerReqVO.java | 28 +++++++++++++++
 .../crm/convert/contact/ContactConvert.java   |  2 +-
 .../crm/convert/contract/ContractConvert.java |  2 +-
 .../convert/customer/CrmCustomerConvert.java  | 18 +++++++---
 .../crm/dal/mysql/clue/CrmClueMapper.java     | 10 +++---
 .../crm/dal/mysql/contact/ContactMapper.java  | 11 +++---
 .../service/business/CrmBusinessService.java  |  2 +-
 .../business/CrmBusinessServiceImpl.java      |  5 +--
 .../crm/service/contact/ContactService.java   |  2 +-
 .../service/contact/ContactServiceImpl.java   |  2 +-
 .../crm/service/contract/ContractService.java |  2 +-
 .../service/contract/ContractServiceImpl.java |  2 +-
 .../service/customer/CrmCustomerService.java  | 16 ++++++---
 .../customer/CrmCustomerServiceImpl.java      | 36 ++++++++++++++++---
 .../business/CrmBusinessServiceImplTest.java  | 18 ----------
 .../service/clue/CrmClueServiceImplTest.java  |  6 ----
 .../customer/CrmCustomerServiceImplTest.java  |  4 ++-
 23 files changed, 129 insertions(+), 70 deletions(-)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/{CrmContactTransferReqVO.java => CrmTransferContactReqVO.java} (96%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/{CrmContractTransferReqVO.java => CrmTransferContractReqVO.java} (59%)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index 754bcd7cf..efff25372 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -92,7 +92,7 @@ public class CrmBusinessController {
     @Operation(summary = "商机转移")
     @PreAuthorize("@ss.hasPermission('crm:business:update')")
     public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferBusinessReqVO reqVO) {
-        businessService.businessTransfer(reqVO, getLoginUserId());
+        businessService.transferBusiness(reqVO, getLoginUserId());
         return success(true);
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
index 98ee287ad..a1872a57a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -100,8 +100,8 @@ public class ContactController {
     @PutMapping("/transfer")
     @Operation(summary = "联系人转移")
     @PreAuthorize("@ss.hasPermission('crm:contact:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContactTransferReqVO reqVO) {
-        contactService.contactTransfer(reqVO, getLoginUserId());
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferContactReqVO reqVO) {
+        contactService.transferContact(reqVO, getLoginUserId());
         return success(true);
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
similarity index 96%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
index db4926e8c..bf58080b3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
@@ -7,7 +7,7 @@ import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 联系人转移 Request VO")
 @Data
-public class CrmContactTransferReqVO {
+public class CrmTransferContactReqVO {
 
     @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     @NotNull(message = "联系人编号不能为空")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
index 3372eb6a3..ae2f0eb61 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
@@ -90,8 +90,8 @@ public class ContractController {
     @PutMapping("/transfer")
     @Operation(summary = "合同转移")
     @PreAuthorize("@ss.hasPermission('crm:contract:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContractTransferReqVO reqVO) {
-        contractService.contractTransfer(reqVO, getLoginUserId());
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferContractReqVO reqVO) {
+        contractService.transferContract(reqVO, getLoginUserId());
         return success(true);
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
similarity index 59%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
index b02834932..67a81ca8d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
@@ -7,7 +7,7 @@ import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 合同转移 Request VO")
 @Data
-public class CrmContractTransferReqVO {
+public class CrmTransferContractReqVO {
 
     @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     @NotNull(message = "合同编号不能为空")
@@ -17,4 +17,12 @@ public class CrmContractTransferReqVO {
     @NotNull(message = "新负责人的用户编号不能为空")
     private Long ownerUserId;
 
+    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "原负责人移除方式不能为空")
+    private Integer transferType;
+
+    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "权限类型不能为空")
+    private Integer permissionType;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 78ae1a20d..4977ef10f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -23,6 +23,7 @@ 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;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - 客户")
 @RestController
@@ -37,7 +38,7 @@ public class CrmCustomerController {
     @Operation(summary = "创建客户")
     @PreAuthorize("@ss.hasPermission('crm:customer:create')")
     public CommonResult<Long> createCustomer(@Valid @RequestBody CrmCustomerCreateReqVO createReqVO) {
-        return success(customerService.createCustomer(createReqVO));
+        return success(customerService.createCustomer(createReqVO, getLoginUserId()));
     }
 
     @PutMapping("/update")
@@ -86,4 +87,12 @@ public class CrmCustomerController {
         ExcelUtils.write(response, "客户.xls", "数据", CrmCustomerExcelVO.class, datas);
     }
 
+    @PutMapping("/transfer")
+    @Operation(summary = "客户转移")
+    @PreAuthorize("@ss.hasPermission('crm:customer:update')")
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferCustomerReqVO reqVO) {
+        customerService.transferCustomer(reqVO, getLoginUserId());
+        return success(true);
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java
new file mode 100644
index 000000000..b452c50e9
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 客户转移 Request VO")
+@Data
+public class CrmTransferCustomerReqVO {
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "客户编号不能为空")
+    private Long id;
+
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long ownerUserId;
+
+    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "原负责人移除方式不能为空")
+    private Integer transferType;
+
+    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "权限类型不能为空")
+    private Integer permissionType;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index 48ebb7d02..9f0e3da36 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -37,6 +37,6 @@ public interface ContactConvert {
             @Mapping(target = "userId", source = "userId"),
             @Mapping(target = "crmDataId", source = "reqVO.id")
     })
-    TransferCrmPermissionBO convert(CrmContactTransferReqVO reqVO, Long userId);
+    TransferCrmPermissionBO convert(CrmTransferContactReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index c962fcd55..bad9fa573 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -37,6 +37,6 @@ public interface ContractConvert {
             @Mapping(target = "userId", source = "userId"),
             @Mapping(target = "crmDataId", source = "reqVO.id")
     })
-    TransferCrmPermissionBO convert(CrmContractTransferReqVO reqVO, Long userId);
+    TransferCrmPermissionBO convert(CrmTransferContractReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index 937705d99..7072536f5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -1,13 +1,15 @@
 package cn.iocoder.yudao.module.crm.convert.customer;
 
-import java.util.*;
-
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
 
 /**
  * 客户 Convert
@@ -29,4 +31,10 @@ public interface CrmCustomerConvert {
 
     List<CrmCustomerExcelVO> convertList02(List<CrmCustomerDO> list);
 
+    @Mappings({
+            @Mapping(target = "userId", source = "userId"),
+            @Mapping(target = "crmDataId", source = "reqVO.id")
+    })
+    TransferCrmPermissionBO convert(CrmTransferCustomerReqVO reqVO, Long userId);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
index 8437337f4..8f9fea6c3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
@@ -1,13 +1,14 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.clue;
 
-import java.util.*;
-
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 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.clue.vo.CrmClueExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import org.apache.ibatis.annotations.Mapper;
-import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+
+import java.util.List;
 
 /**
  * 线索 Mapper
@@ -35,7 +36,6 @@ public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
                 .likeIfPresent(CrmClueDO::getTelephone, reqVO.getTelephone())
                 .likeIfPresent(CrmClueDO::getMobile, reqVO.getMobile())
                 .likeIfPresent(CrmClueDO::getAddress, reqVO.getAddress())
-                .eqIfPresent(CrmClueDO::getOwnerUserId, reqVO.getOwnerUserId())
                 .betweenIfPresent(CrmClueDO::getContactLastTime, reqVO.getContactLastTime())
                 .betweenIfPresent(CrmClueDO::getCreateTime, reqVO.getCreateTime())
                 .orderByDesc(CrmClueDO::getId));
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
index dfefeba5d..7643e4d4c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
@@ -1,13 +1,14 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.contact;
 
-import java.util.*;
-
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 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.contact.vo.ContactExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactPageReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import org.apache.ibatis.annotations.Mapper;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+
+import java.util.List;
 
 /**
  * crm联系人 Mapper
@@ -28,7 +29,6 @@ public interface ContactMapper extends BaseMapperX<ContactDO> {
                 .eqIfPresent(ContactDO::getCustomerId, reqVO.getCustomerId())
                 .eqIfPresent(ContactDO::getAddress, reqVO.getAddress())
                 .eqIfPresent(ContactDO::getRemark, reqVO.getRemark())
-                .eqIfPresent(ContactDO::getOwnerUserId, reqVO.getOwnerUserId())
                 .betweenIfPresent(ContactDO::getCreateTime, reqVO.getCreateTime())
                 .betweenIfPresent(ContactDO::getLastTime, reqVO.getLastTime())
                 .orderByDesc(ContactDO::getId));
@@ -45,7 +45,6 @@ public interface ContactMapper extends BaseMapperX<ContactDO> {
                 .eqIfPresent(ContactDO::getCustomerId, reqVO.getCustomerId())
                 .eqIfPresent(ContactDO::getAddress, reqVO.getAddress())
                 .eqIfPresent(ContactDO::getRemark, reqVO.getRemark())
-                .eqIfPresent(ContactDO::getOwnerUserId, reqVO.getOwnerUserId())
                 .betweenIfPresent(ContactDO::getCreateTime, reqVO.getCreateTime())
                 .betweenIfPresent(ContactDO::getLastTime, reqVO.getLastTime())
                 .orderByDesc(ContactDO::getId));
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index 233b6603a..63baad383 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -76,6 +76,6 @@ public interface CrmBusinessService {
      * @param reqVO  请求
      * @param userId 用户编号
      */
-    void businessTransfer(CrmTransferBusinessReqVO reqVO, Long userId);
+    void transferBusiness(CrmTransferBusinessReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index 250f01492..2885e862d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -12,7 +12,6 @@ import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
-import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -36,8 +35,6 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     @Resource
     private CrmBusinessMapper businessMapper;
 
-    @Resource
-    private AdminUserApi adminUserApi;
     @Resource
     private CrmPermissionService crmPermissionService;
 
@@ -111,7 +108,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void businessTransfer(CrmTransferBusinessReqVO reqVO, Long userId) {
+    public void transferBusiness(CrmTransferBusinessReqVO reqVO, Long userId) {
         // 1 校验商机是否存在
         validateBusinessExists(reqVO.getId());
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
index a66edc650..6da77138e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
@@ -76,6 +76,6 @@ public interface ContactService {
      * @param reqVO  请求
      * @param userId 用户编号
      */
-    void contactTransfer(CrmContactTransferReqVO reqVO, Long userId);
+    void transferContact(CrmTransferContactReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index d39ada967..aea47ecb4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -109,7 +109,7 @@ public class ContactServiceImpl implements ContactService {
     }
 
     @Override
-    public void contactTransfer(CrmContactTransferReqVO reqVO, Long userId) {
+    public void transferContact(CrmTransferContactReqVO reqVO, Long userId) {
         // 1 校验联系人是否存在
         validateContactExists(reqVO.getId());
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
index 969f77851..8cf41c152 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
@@ -76,6 +76,6 @@ public interface ContractService {
      * @param reqVO  请求
      * @param userId 用户编号
      */
-    void contractTransfer(CrmContractTransferReqVO reqVO, Long userId);
+    void transferContract(CrmTransferContractReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index f07911455..fa3940f6a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -107,7 +107,7 @@ public class ContractServiceImpl implements ContractService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void contractTransfer(CrmContractTransferReqVO reqVO, Long userId) {
+    public void transferContract(CrmTransferContractReqVO reqVO, Long userId) {
         // 1 校验合同是否存在
         validateContractExists(reqVO.getId());
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index 73c6af2b6..576a9bcbc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -1,10 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerUpdateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 
 import javax.validation.Valid;
@@ -22,9 +19,10 @@ public interface CrmCustomerService {
      * 创建客户
      *
      * @param createReqVO 创建信息
+     * @param userId      用户编号
      * @return 编号
      */
-    Long createCustomer(@Valid CrmCustomerCreateReqVO createReqVO);
+    Long createCustomer(@Valid CrmCustomerCreateReqVO createReqVO, Long userId);
 
     /**
      * 更新客户
@@ -80,4 +78,12 @@ public interface CrmCustomerService {
      */
     CrmCustomerDO validateCustomer(Long customerId);
 
+    /**
+     * 客户转移
+     *
+     * @param reqVO  请求
+     * @param userId 用户编号
+     */
+    void transferCustomer(CrmTransferCustomerReqVO reqVO, Long userId);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 15535f681..879425a1c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -3,15 +3,18 @@ package cn.iocoder.yudao.module.crm.service.customer;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerUpdateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
+import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
@@ -35,17 +38,26 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     private CrmCustomerMapper customerMapper;
     @Resource
     private DeptApi deptApi; // TODO @wanwan:拼接数据,可以放到 controller;所以这里的引入,可以考虑放到 controller 哈;
+    @Resource
+    private CrmPermissionService crmPermissionService;
 
     @Override
-    public Long createCustomer(CrmCustomerCreateReqVO createReqVO) {
+    public Long createCustomer(CrmCustomerCreateReqVO createReqVO, Long userId) {
         // 插入
         CrmCustomerDO customer = CrmCustomerConvert.INSTANCE.convert(createReqVO);
         customerMapper.insert(customer);
+
+        // 创建数据权限
+        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmEnum.CRM_CUSTOMER.getType())
+                .setCrmDataId(customer.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
+
         // 返回
         return customer.getId();
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(crmType = CrmEnum.CRM_CUSTOMER, operationType = OperationTypeEnum.UPDATE)
     public void updateCustomer(CrmCustomerUpdateReqVO updateReqVO) {
         // 校验存在
         validateCustomerExists(updateReqVO.getId());
@@ -57,6 +69,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(crmType = CrmEnum.CRM_CUSTOMER, operationType = OperationTypeEnum.DELETE)
     public void deleteCustomer(Long id) {
         // 校验存在
         validateCustomerExists(id);
@@ -73,6 +87,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     @Override
+    @CrmPermission(crmType = CrmEnum.CRM_CUSTOMER, operationType = OperationTypeEnum.READ)
     public CrmCustomerDO getCustomer(Long id) {
         return customerMapper.selectById(id);
     }
@@ -112,4 +127,15 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         return customer;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void transferCustomer(CrmTransferCustomerReqVO reqVO, Long userId) {
+        // 1 校验合同是否存在
+        validateCustomer(reqVO.getId());
+
+        // 2. 数据权限转移
+        crmPermissionService.transferCrmPermission(
+                CrmCustomerConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_CUSTOMER.getType()));
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
index b37563ada..9f199954e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImplTest.java
@@ -115,10 +115,7 @@ public class CrmBusinessServiceImplTest extends BaseDbUnitTest {
            o.setDiscountPercent(null);
            o.setProductPrice(null);
            o.setRemark(null);
-           o.setOwnerUserId(null);
            o.setCreateTime(null);
-           o.setRoUserIds(null);
-           o.setRwUserIds(null);
            o.setEndStatus(null);
            o.setEndRemark(null);
            o.setContactLastTime(null);
@@ -145,14 +142,8 @@ public class CrmBusinessServiceImplTest extends BaseDbUnitTest {
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setProductPrice(null)));
        // 测试 remark 不匹配
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRemark(null)));
-       // 测试 ownerUserId 不匹配
-       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setOwnerUserId(null)));
        // 测试 createTime 不匹配
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setCreateTime(null)));
-       // 测试 roUserIds 不匹配
-       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRoUserIds(null)));
-       // 测试 rwUserIds 不匹配
-       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRwUserIds(null)));
        // 测试 endStatus 不匹配
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setEndStatus(null)));
        // 测试 endRemark 不匹配
@@ -205,10 +196,7 @@ public class CrmBusinessServiceImplTest extends BaseDbUnitTest {
            o.setDiscountPercent(null);
            o.setProductPrice(null);
            o.setRemark(null);
-           o.setOwnerUserId(null);
            o.setCreateTime(null);
-           o.setRoUserIds(null);
-           o.setRwUserIds(null);
            o.setEndStatus(null);
            o.setEndRemark(null);
            o.setContactLastTime(null);
@@ -235,14 +223,8 @@ public class CrmBusinessServiceImplTest extends BaseDbUnitTest {
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setProductPrice(null)));
        // 测试 remark 不匹配
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRemark(null)));
-       // 测试 ownerUserId 不匹配
-       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setOwnerUserId(null)));
        // 测试 createTime 不匹配
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setCreateTime(null)));
-       // 测试 roUserIds 不匹配
-       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRoUserIds(null)));
-       // 测试 rwUserIds 不匹配
-       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setRwUserIds(null)));
        // 测试 endStatus 不匹配
        businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setEndStatus(null)));
        // 测试 endRemark 不匹配
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
index 63a838164..4757e921d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImplTest.java
@@ -115,7 +115,6 @@ public class CrmClueServiceImplTest extends BaseDbUnitTest {
            o.setTelephone(null);
            o.setMobile(null);
            o.setAddress(null);
-           o.setOwnerUserId(null);
            o.setContactLastTime(null);
            o.setCreateTime(null);
        });
@@ -136,8 +135,6 @@ public class CrmClueServiceImplTest extends BaseDbUnitTest {
        clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setMobile(null)));
        // 测试 address 不匹配
        clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setAddress(null)));
-       // 测试 ownerUserId 不匹配
-       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setOwnerUserId(null)));
        // 测试 contactLastTime 不匹配
        clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactLastTime(null)));
        // 测试 createTime 不匹配
@@ -169,7 +166,6 @@ public class CrmClueServiceImplTest extends BaseDbUnitTest {
            o.setTelephone(null);
            o.setMobile(null);
            o.setAddress(null);
-           o.setOwnerUserId(null);
            o.setContactLastTime(null);
            o.setCreateTime(null);
        });
@@ -190,8 +186,6 @@ public class CrmClueServiceImplTest extends BaseDbUnitTest {
        clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setMobile(null)));
        // 测试 address 不匹配
        clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setAddress(null)));
-       // 测试 ownerUserId 不匹配
-       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setOwnerUserId(null)));
        // 测试 contactLastTime 不匹配
        clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactLastTime(null)));
        // 测试 createTime 不匹配
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
index aa188f06a..41dd14f92 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
@@ -16,6 +16,7 @@ import javax.annotation.Resource;
 import java.util.List;
 
 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;
@@ -24,6 +25,7 @@ import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_
 import static org.junit.jupiter.api.Assertions.*;
 
 // TODO 芋艿:单测后续补
+
 /**
  * {@link CrmCustomerServiceImpl} 的单元测试类
  *
@@ -44,7 +46,7 @@ public class CrmCustomerServiceImplTest extends BaseDbUnitTest {
         CrmCustomerCreateReqVO reqVO = randomPojo(CrmCustomerCreateReqVO.class);
 
         // 调用
-        Long customerId = customerService.createCustomer(reqVO);
+        Long customerId = customerService.createCustomer(reqVO, getLoginUserId());
         // 断言
         assertNotNull(customerId);
         // 校验记录的属性是否正确

From 4dab376ca968941b99ca75029193698ae36f00fb Mon Sep 17 00:00:00 2001
From: "zhijiantianya@gmail.com" <zhijiantianya@gmail.com>
Date: Mon, 30 Oct 2023 19:28:33 +0800
Subject: [PATCH 044/101] =?UTF-8?q?code=20review=EF=BC=9A=E6=95=B0?=
 =?UTF-8?q?=E6=8D=AE=E6=9D=83=E9=99=90=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../yudao/module/crm/enums/ErrorCodeConstants.java |  1 +
 .../module/crm/enums/common/TransferTypeEnum.java  |  2 ++
 .../admin/contact/ContactController.java           |  2 +-
 .../admin/contact/vo/ContactCreateReqVO.java       |  2 +-
 .../admin/contact/vo/ContactExportReqVO.java       |  2 +-
 .../admin/contact/vo/ContactPageReqVO.java         |  2 +-
 .../controller/admin/contact/vo/ContactRespVO.java |  2 +-
 .../admin/contact/vo/ContactUpdateReqVO.java       |  2 +-
 .../admin/contact/vo/CrmTransferContactReqVO.java  |  2 +-
 .../admin/contract/ContractController.java         |  2 +-
 .../controller/admin/contract/package-info.java    |  4 ----
 .../admin/contract/vo/ContractCreateReqVO.java     |  2 +-
 .../admin/contract/vo/ContractExcelVO.java         |  2 +-
 .../admin/contract/vo/ContractExportReqVO.java     |  2 +-
 .../admin/contract/vo/ContractPageReqVO.java       |  2 +-
 .../admin/contract/vo/ContractRespVO.java          |  2 +-
 .../admin/contract/vo/ContractUpdateReqVO.java     |  2 +-
 .../contract/vo/CrmTransferContractReqVO.java      |  3 ++-
 .../admin/customer/CrmCustomerController.java      |  2 +-
 .../admin/customer/vo/CrmCustomerCreateReqVO.java  |  2 +-
 .../admin/customer/vo/CrmCustomerExcelVO.java      |  2 +-
 .../admin/customer/vo/CrmCustomerExportReqVO.java  |  2 +-
 .../admin/customer/vo/CrmCustomerPageReqVO.java    |  2 +-
 .../admin/customer/vo/CrmCustomerRespVO.java       |  2 +-
 .../admin/customer/vo/CrmCustomerUpdateReqVO.java  |  2 +-
 .../customer/vo/CrmTransferCustomerReqVO.java      |  2 +-
 .../admin/receivable/ReceivableController.java     |  5 +----
 .../admin/receivable/ReceivablePlanController.java |  5 +----
 .../admin/receivable/vo/ReceivableCreateReqVO.java |  2 +-
 .../admin/receivable/vo/ReceivableExcelVO.java     |  2 +-
 .../admin/receivable/vo/ReceivableExportReqVO.java |  2 +-
 .../admin/receivable/vo/ReceivablePageReqVO.java   |  2 +-
 .../receivable/vo/ReceivablePlanCreateReqVO.java   |  2 +-
 .../admin/receivable/vo/ReceivablePlanExcelVO.java |  8 ++------
 .../receivable/vo/ReceivablePlanExportReqVO.java   |  2 +-
 .../receivable/vo/ReceivablePlanPageReqVO.java     |  2 +-
 .../admin/receivable/vo/ReceivablePlanRespVO.java  |  2 +-
 .../receivable/vo/ReceivablePlanUpdateReqVO.java   |  2 +-
 .../admin/receivable/vo/ReceivableRespVO.java      |  2 +-
 .../admin/receivable/vo/ReceivableUpdateReqVO.java |  2 +-
 .../module/crm/convert/contact/ContactConvert.java |  2 +-
 .../framework/core/aop/CrmPermissionAspect.java    |  9 ++++++---
 .../yudao/module/crm/framework/enums/CrmEnum.java  |  1 +
 .../crm/framework/enums/OperationTypeEnum.java     |  2 ++
 .../service/customer/CrmCustomerServiceImpl.java   |  6 +++---
 .../service/permission/CrmPermissionService.java   |  1 +
 .../permission/CrmPermissionServiceImpl.java       | 14 ++++++++++----
 .../permission/bo/CrmPermissionCreateBO.java       |  4 ++++
 .../permission/bo/CrmPermissionUpdateBO.java       |  2 ++
 .../permission/bo/TransferCrmPermissionBO.java     | 11 ++++++++---
 50 files changed, 80 insertions(+), 66 deletions(-)
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/package-info.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index aba62797d..0977faa70 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -34,6 +34,7 @@ public interface ErrorCodeConstants {
     // ========== 客户管理 1_020_006_000 ==========
     ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
 
+    // TODO @puhui999:权限管理???
     // ========== 客户管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
     ErrorCode CRM_PERMISSION_DENIED = new ErrorCode(1_020_007_001, "{}操作失败,原因:没有权限");
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
index d12ce71db..7a4396265 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
@@ -6,6 +6,7 @@ import lombok.Getter;
 
 import java.util.Arrays;
 
+// TODO @puhui999:这个可以不用哈
 /**
  * Crm 负责人转移后原负责人的处理方式
  *
@@ -17,6 +18,7 @@ public enum TransferTypeEnum implements IntArrayValuable {
 
     REMOVE(1, "移除"),
     TEAM(2, "转为团队成员");
+
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TransferTypeEnum::getType).toArray();
 
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
index a1872a57a..cda6b29b9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -26,7 +26,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
-@Tag(name = "管理后台 - crm联系人")
+@Tag(name = "管理后台 - CRM 联系人")
 @RestController
 @RequestMapping("/crm/contact")
 @Validated
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
index 9f4d8ec2c..5eccfea74 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
@@ -5,7 +5,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-@Schema(description = "管理后台 - crm联系人创建 Request VO")
+@Schema(description = "管理后台 - CRM 联系人创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
index df4bd59e1..961cebd0c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
@@ -9,7 +9,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - crm联系人 Excel 导出 Request VO,参数和 ContactPageReqVO 是一致的")
+@Schema(description = "管理后台 - CRM 联系人 Excel 导出 Request VO,参数和 ContactPageReqVO 是一致的")
 @Data
 public class ContactExportReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
index 31ad2a1fb..64d8a04b9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - crm联系人分页 Request VO")
+@Schema(description = "管理后台 - CRM 联系人分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
index acb6a737d..6a02c69d2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
@@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import java.time.LocalDateTime;
 
-@Schema(description = "管理后台 - crm联系人 Response VO")
+@Schema(description = "管理后台 - CRM 联系人 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
index f319b52f0..d2621fa5e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
@@ -5,7 +5,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-@Schema(description = "管理后台 - crm联系人更新 Request VO")
+@Schema(description = "管理后台 - CRM 联系人更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
index bf58080b3..1e16d2d20 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
@@ -5,7 +5,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 
-@Schema(description = "管理后台 - 联系人转移 Request VO")
+@Schema(description = "管理后台 - CRM 联系人转移 Request VO")
 @Data
 public class CrmTransferContactReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
index ae2f0eb61..d875b21e3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
@@ -25,7 +25,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
-@Tag(name = "管理后台 - 合同")
+@Tag(name = "管理后台 - CRM 合同")
 @RestController
 @RequestMapping("/crm/contract")
 @Validated
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/package-info.java
deleted file mode 100644
index b2f4b7b4a..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * 合同
- */
-package cn.iocoder.yudao.module.crm.controller.admin.contract;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
index b21007f47..7793d7737 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
@@ -5,7 +5,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-@Schema(description = "管理后台 - 合同创建 Request VO")
+@Schema(description = "管理后台 - CRM 合同创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java
index 895ac821a..2fb521321 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java
@@ -6,7 +6,7 @@ import lombok.Data;
 import java.time.LocalDateTime;
 
 /**
- * 合同 Excel VO
+ * CRM 合同 Excel VO
  *
  * @author dhb52
  */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java
index 8de3b4c1b..003e1f57c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java
@@ -8,7 +8,7 @@ import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - 合同 Excel 导出 Request VO,参数和 ContractPageReqVO 是一致的")
+@Schema(description = "管理后台 - CRM 合同 Excel 导出 Request VO,参数和 ContractPageReqVO 是一致的")
 @Data
 public class ContractExportReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java
index 01562be1b..36c7e14be 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java
@@ -11,7 +11,7 @@ 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")
+@Schema(description = "管理后台 - CRM 合同分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java
index a0ab1d248..4a22251b0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java
@@ -7,7 +7,7 @@ import lombok.ToString;
 
 import java.time.LocalDateTime;
 
-@Schema(description = "管理后台 - 合同 Response VO")
+@Schema(description = "管理后台 - CRM 合同 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
index ba38ca383..34a9797f4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
@@ -7,7 +7,7 @@ import lombok.ToString;
 
 import javax.validation.constraints.NotNull;
 
-@Schema(description = "管理后台 - 合同更新 Request VO")
+@Schema(description = "管理后台 - CRM 合同更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
index 67a81ca8d..a987c9f89 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
@@ -5,7 +5,8 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 
-@Schema(description = "管理后台 - 合同转移 Request VO")
+// TODO @puhui999:CrmContractTransferReqVO,模块名字要放前面;看看还有没其它类似的
+@Schema(description = "管理后台 - CRM 合同转移 Request VO")
 @Data
 public class CrmTransferContractReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 4977ef10f..ff144ffbf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -25,7 +25,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
-@Tag(name = "管理后台 - 客户")
+@Tag(name = "管理后台 - CRM 客户")
 @RestController
 @RequestMapping("/crm/customer")
 @Validated
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
index 84462ddfa..38108776a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
@@ -5,7 +5,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-@Schema(description = "管理后台 - 客户创建 Request VO")
+@Schema(description = "管理后台 - CRM 客户创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
index 0fee3df4c..6f8156175 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
 
 // TODO 芋艿:导出最后做,等基本确认的差不多之后;
 /**
- * 客户 Excel VO
+ * CRM 客户 Excel VO
  *
  * @author Wanwan
  */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
index 03a3f2d4d..3a37c2834 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
@@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 // TODO 芋艿:导出最后做,等基本确认的差不多之后;
-@Schema(description = "管理后台 - 客户 Excel 导出 Request VO,参数和 CrmCustomerPageReqVO 是一致的")
+@Schema(description = "管理后台 - CRM 客户 Excel 导出 Request VO,参数和 CrmCustomerPageReqVO 是一致的")
 @Data
 public class CrmCustomerExportReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
index 8bf2018f1..e8a0b9e71 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
@@ -6,7 +6,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-@Schema(description = "管理后台 - 客户分页 Request VO")
+@Schema(description = "管理后台 - CRM 客户分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
index bdcf28f2c..505221dfd 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - 客户 Response VO")
+@Schema(description = "管理后台 - CRM 客户 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
index 545643fd9..6ed1566b3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerUpdateReqVO.java
@@ -7,7 +7,7 @@ import lombok.ToString;
 
 import javax.validation.constraints.NotNull;
 
-@Schema(description = "管理后台 - 客户更新 Request VO")
+@Schema(description = "管理后台 - CRM 客户更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java
index b452c50e9..78ca15aca 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java
@@ -5,7 +5,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 
-@Schema(description = "管理后台 - 客户转移 Request VO")
+@Schema(description = "管理后台 - CRM 客户转移 Request VO")
 @Data
 public class CrmTransferCustomerReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
index 564e06bdb..c88f31f6d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
@@ -24,10 +24,7 @@ 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;
 
-/**
- * @author 赤焰
- */
-@Tag(name = "管理后台 - 回款管理")
+@Tag(name = "管理后台 - CRM 回款管理")
 @RestController
 @RequestMapping("/crm/receivable")
 @Validated
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
index 9bf8d916a..0c33eb25a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
@@ -24,10 +24,7 @@ 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;
 
-/**
- * @author 赤焰
- */
-@Tag(name = "管理后台 - 回款计划")
+@Tag(name = "管理后台 - CRM 回款计划")
 @RestController
 @RequestMapping("/crm/receivable-plan")
 @Validated
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java
index 5bbb4fce2..d38d0bd5f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java
@@ -5,7 +5,7 @@ import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
 import javax.validation.constraints.*;
 
-@Schema(description = "管理后台 - 回款管理创建 Request VO")
+@Schema(description = "管理后台 - CRM 回款创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
index f60330c53..b525341d3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
@@ -14,7 +14,7 @@ import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 
 
 /**
- * 回款管理 Excel VO
+ * CRM 回款管理 Excel VO
  *
  * @author 赤焰
  */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
index 7ea31a0fb..4a9a033f3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
@@ -12,7 +12,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 /**
  * @author 赤焰
  */
-@Schema(description = "管理后台 - 回款管理 Excel 导出 Request VO,参数和 ReceivablePageReqVO 是一致的")
+@Schema(description = "管理后台 - CRM 回款 Excel 导出 Request VO,参数和 ReceivablePageReqVO 是一致的")
 @Data
 public class ReceivableExportReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
index 7abd7cb39..ba4775535 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
@@ -12,7 +12,7 @@ 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")
+@Schema(description = "管理后台 - CRM 回款分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java
index 0d4fce3c8..d03e76eb0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java
@@ -5,7 +5,7 @@ import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
 import javax.validation.constraints.*;
 
-@Schema(description = "管理后台 - 回款计划创建 Request VO")
+@Schema(description = "管理后台 - CRM 回款计划创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
index e12d5a1df..f0b822115 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
@@ -1,13 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
-import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
-import java.util.*;
+
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
 
 import com.alibaba.excel.annotation.ExcelProperty;
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
@@ -15,7 +11,7 @@ import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 
 
 /**
- * 回款计划 Excel VO
+ * CRM 回款计划 Excel VO
  *
  * @author 芋道源码
  */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
index ca5d3f553..506ffa1ed 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
@@ -9,7 +9,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - 回款计划 Excel 导出 Request VO,参数和 ReceivablePlanPageReqVO 是一致的")
+@Schema(description = "管理后台 - CRM 回款计划 Excel 导出 Request VO,参数和 ReceivablePlanPageReqVO 是一致的")
 @Data
 public class ReceivablePlanExportReqVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
index 2d0e7d5ae..16ad6353d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
@@ -11,7 +11,7 @@ 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")
+@Schema(description = "管理后台 - CRM 回款计划分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java
index d96f631de..ce49d5977 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java
@@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import java.time.LocalDateTime;
 
-@Schema(description = "管理后台 - 回款计划 Response VO")
+@Schema(description = "管理后台 - CRM 回款计划 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java
index 75f4ecca0..1f539537d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java
@@ -5,7 +5,7 @@ import lombok.*;
 import java.util.*;
 import javax.validation.constraints.*;
 
-@Schema(description = "管理后台 - 回款计划更新 Request VO")
+@Schema(description = "管理后台 - CRM 回款计划更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java
index fd26e312e..1646cd5a8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java
@@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import java.time.LocalDateTime;
 
-@Schema(description = "管理后台 - 回款管理 Response VO")
+@Schema(description = "管理后台 - CRM 回款 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java
index 600b09943..008c06b63 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java
@@ -5,7 +5,7 @@ import lombok.*;
 import java.util.*;
 import javax.validation.constraints.*;
 
-@Schema(description = "管理后台 - 回款管理更新 Request VO")
+@Schema(description = "管理后台 - CRM 回款更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index 9f0e3da36..11b549062 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -12,7 +12,7 @@ import org.mapstruct.factory.Mappers;
 import java.util.List;
 
 /**
- * crm联系人 Convert
+ * crm 联系人 Convert
  *
  * @author 芋道源码
  */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index dd25703e0..48d29c6c4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -47,19 +47,21 @@ public class CrmPermissionAspect {
         try {
             Integer crmType = crmPermission.crmType().getType();
             Integer operationType = crmPermission.operationType().getType();
+            // TODO @puhui999:不一定是 id 参数噢;例如说,ContactServiceImpl 的 updateContact
             Long id = (Long) joinPoint.getArgs()[0];// 获取操作数据的编号
 
-            // 1. 获取数据权限
+            // 1.1 获取数据权限
             CrmPermissionDO permission = crmPermissionService.getCrmPermissionByCrmTypeAndCrmDataId(crmType, id);
             if (permission == null) {
                 // 不存在说明数据也不存在
                 throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, crmPermission.crmType().getName());
             }
-            // 1.2. 校验是否为公海数据
+            // 1.2 校验是否为公海数据
+            // TODO @puhui999:这个判断去掉比较合适哈。这里更多是业务逻辑,不算权限判断。例如说,公海的客户,只要没负责人,就可以领取了;
             if (permission.getOwnerUserId() == null) {
                 return;
             }
-            // 1.3. 校验当前负责人是不是自己
+            // 1.3 校验当前负责人是不是自己
             if (ObjUtil.equal(permission.getOwnerUserId(), getUserId())) {
                 return;
             }
@@ -68,6 +70,7 @@ public class CrmPermissionAspect {
             // 2. 校验是否有读权限
             if (OperationTypeEnum.isRead(operationType)) {
                 // 校验该数据当前用户是否可读
+                // TODO @puhui999:直接 CollUtil.contains 就好,因为就是有某个 userId 呀
                 boolean isRead = CollUtil.contains(permission.getRoUserIds(), item -> ObjUtil.equal(item, getUserId()))
                         || CollUtil.contains(permission.getRwUserIds(), item -> ObjUtil.equal(item, getUserId()));
                 if (isRead) {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
index 62fd4d007..eb6110ec0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjUtil;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 
+// TODO @puhui999:可以改成 CrmBizTypeEnum,CRM 业务类型枚举
 /**
  * Crm 类型枚举
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
index 3cc2e8b6e..893717154 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjUtil;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 
+// TODO @puhui999:是不是可以和 PermissionTypeEnum 合并,就是 CrmPermissionEnum,负责人、读取、读写;目前阶段,不用做的特别细致;类似 linux 的 acl;
 /**
  * Crm 数据操作类型枚举
  *
@@ -13,6 +14,7 @@ import lombok.RequiredArgsConstructor;
 @Getter
 public enum OperationTypeEnum {
 
+    // TODO @puhui999:抽象上,就分三种,会更合理。一个 OWNER 负责人,一个 READ 读,一个 WRITE 写;
     DELETE(1, "删除"),
     UPDATE(2, "修改"),
     READ(3, "查询");
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 879425a1c..e78717b2c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -115,8 +115,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     /**
      * 校验客户是否存在
      *
-     * @param customerId 客户id
-     * @return
+     * @param customerId 客户 id
+     * @return 客户
      */
     @Override
     public CrmCustomerDO validateCustomer(Long customerId) {
@@ -130,7 +130,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void transferCustomer(CrmTransferCustomerReqVO reqVO, Long userId) {
-        // 1 校验合同是否存在
+        // 1. 校验合同是否存在
         validateCustomer(reqVO.getId());
 
         // 2. 数据权限转移
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index 91321b709..3203c06bc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -16,6 +16,7 @@ import javax.validation.Valid;
  */
 public interface CrmPermissionService {
 
+    // TODO @puhui999:方法名上,不用 Crm
     /**
      * 创建数据权限
      *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index 2647976bc..687de63ca 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -41,6 +41,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createCrmPermission(CrmPermissionCreateBO createBO) {
+        // TODO @puhui999:createDO 改成 permission,保持统一哈;
         CrmPermissionDO createDO = CrmPermissionConvert.INSTANCE.convert(createBO);
         crmPermissionMapper.insert(createDO);
         return createDO.getId();
@@ -50,7 +51,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Transactional(rollbackFor = Exception.class)
     public void updateCrmPermission(CrmPermissionUpdateBO updateBO) {
         validateCrmPermissionExists(updateBO.getId());
-
+        // 更新操作
         CrmPermissionDO updateDO = CrmPermissionConvert.INSTANCE.convert(updateBO);
         crmPermissionMapper.updateById(updateDO);
     }
@@ -59,7 +60,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Transactional(rollbackFor = Exception.class)
     public void deleteCrmPermission(Long id) {
         validateCrmPermissionExists(id);
-
+        // 删除
         crmPermissionMapper.deleteById(id);
     }
 
@@ -74,9 +75,11 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         return crmPermissionMapper.selectByCrmTypeAndCrmDataId(crmType, crmDataId);
     }
 
+    // TODO @puhui999:参数名,是不是 transferReqBO
     @Override
     public void transferCrmPermission(TransferCrmPermissionBO transferCrmPermissionBO) {
-        // 1 校验商机是否存在
+        // 1.1 校验商机是否存在
+        // TODO puhui999:这里直接调用 crmPermissionMapper 的 selectByCrmTypeAndCrmDataId 方法,会更简洁一点;
         CrmPermissionDO permission = getCrmPermissionByCrmTypeAndCrmDataId(transferCrmPermissionBO.getCrmType(),
                 transferCrmPermissionBO.getCrmDataId());
         String crmName = CrmEnum.getNameByType(transferCrmPermissionBO.getCrmType());
@@ -85,6 +88,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         }
         // 1.2 校验转移对象是否已经是该负责人
         if (ObjUtil.equal(permission.getOwnerUserId(), permission.getOwnerUserId())) {
+            // TODO @puhui999:是不是这个错误码不太对。。。
             throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS, crmName);
         }
         // 1.3 校验新负责人是否存在
@@ -94,11 +98,12 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         }
         // TODO 校验是否为超级管理员 || 1.4
         // 1.4 校验是否有写权限
+        // TODO puhui999:CollUtil.contains 就够了,不用后面写个表达式;
         if (!CollUtil.contains(permission.getRwUserIds(), id -> ObjUtil.equal(id, transferCrmPermissionBO.getUserId()))) {
             throw exception(CRM_PERMISSION_DENIED, crmName);
         }
 
-        // 2 权限转移
+        // 2. 权限转移
         CrmPermissionDO updateCrmPermission = new CrmPermissionDO().setId(permission.getId())
                 .setOwnerUserId(transferCrmPermissionBO.getOwnerUserId());
         if (ObjUtil.equal(TransferTypeEnum.TEAM.getType(), transferCrmPermissionBO.getTransferType())) {
@@ -116,6 +121,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         crmPermissionMapper.updateById(updateCrmPermission);
 
         // 3. TODO 记录机转移日志
+        // TODO @puhui999:是不是交给业务记录哈;
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
index b7d693a18..e5dd5d6e9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
@@ -6,6 +6,7 @@ import lombok.Data;
 import javax.validation.constraints.NotNull;
 import java.util.Set;
 
+// TODO @puhui999:,一个是 Crm 前缀,一个 Req 表示入参
 /**
  * crm 数据权限 Create BO
  *
@@ -14,6 +15,8 @@ import java.util.Set;
 @Data
 public class CrmPermissionCreateBO {
 
+    // TODO @puhui999:如果是关联字段,换一行写它的注释;不然看着略乱哈
+
     /**
      * Crm 类型 关联 {@link CrmEnum}
      */
@@ -24,6 +27,7 @@ public class CrmPermissionCreateBO {
      */
     @NotNull(message = "Crm 数据编号不能为空")
     private Long crmDataId;
+
     /**
      * 负责人的用户编号 关联 AdminUser#id, null 则为公海数据
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
index 6cad08e75..e8544a611 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
@@ -7,6 +7,7 @@ import lombok.Data;
 import javax.validation.constraints.NotNull;
 import java.util.Set;
 
+// TODO @puhui999:,一个是 Crm 前缀,一个 Req 表示入参
 /**
  * crm 数据权限 Update BO
  *
@@ -15,6 +16,7 @@ import java.util.Set;
 @Data
 public class CrmPermissionUpdateBO {
 
+    // TODO @puhui999:id 和 crmType + crmDataId 是不是重叠了;
     /**
      * 数据权限编号 {@link CrmPermissionDO#getId()}
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java
index 2f3e94f38..246439bcb 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java
@@ -7,6 +7,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 
+// TODO @puhui999:CrmTransferPermissionReqBO,一个是 Crm 前缀,一个 Req 表示入参
 /**
  * 数据权限转移 BO
  *
@@ -15,26 +16,30 @@ import javax.validation.constraints.NotNull;
 @Data
 public class TransferCrmPermissionBO {
 
+    // TODO @puhui999:参数的注释
     @NotNull(message = "用户编号不能为空")
     private Long userId;
 
+    // TODO @puhui999:bizType
     /**
-     * Crm 类型 关联 {@link CrmEnum}
+     * Crm 类型 关联 {@link CrmEnum} TODO 这种不用再写关联了,直接 @InEnum 参数校验
      */
     @NotNull(message = "Crm 类型不能为空")
     private Integer crmType;
 
+    // TODO @puhui999:bizId
     /**
-     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
+     * 数据编号
      */
     @NotNull(message = "Crm 数据编号不能为空")
     private Long crmDataId;
 
+    // TODO @puhui999:要不这里改成 newOwnerUserId;然后,transferType 和 permissionType,合并成 oldOwnerPermission(空就是移除)
     @NotNull(message = "新负责人的用户编号不能为空")
     private Long ownerUserId;
 
     /**
-     * 原负责人移除方式, 关联 {@link TransferTypeEnum}
+     * 原负责人移除方式, 关联 {@link TransferTypeEnum} TODO 这种不用再写关联了,直接 @InEnum 参数校验
      */
     @NotNull(message = "原负责人移除方式不能为空")
     private Integer transferType;

From 921c2ab7571b3dc84cb1dff808a1f469d35178b4 Mon Sep 17 00:00:00 2001
From: liuhongfeng <291117974@qq.com>
Date: Mon, 30 Oct 2023 21:33:28 +0800
Subject: [PATCH 045/101] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91?=
 =?UTF-8?q?=E8=B4=9F=E8=B4=A3=E4=BA=BA=20=E3=80=90=E4=BF=AE=E6=94=B9?=
 =?UTF-8?q?=E3=80=91=E8=A7=84=E8=8C=83=E5=91=BD=E5=90=8D=EF=BC=9A=E6=9C=9F?=
 =?UTF-8?q?=E6=95=B0=EF=BC=9AindexNo-=E3=80=8Bperiod?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                                |  2 +-
 .../receivable/vo/ReceivablePlanBaseVO.java      |  2 +-
 .../receivable/vo/ReceivablePlanExcelVO.java     |  8 ++------
 .../receivable/vo/ReceivablePlanExportReqVO.java |  5 ++---
 .../receivable/vo/ReceivablePlanPageReqVO.java   |  2 +-
 .../dataobject/receivable/ReceivablePlanDO.java  |  8 +++-----
 .../mysql/receivable/ReceivablePlanMapper.java   |  4 ++--
 .../ReceivablePlanServiceImplTest.java           | 16 ++++++++--------
 8 files changed, 20 insertions(+), 27 deletions(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index de7922d24..8c662c238 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -41,7 +41,7 @@ CREATE TABLE `crm_receivable`  (
 DROP TABLE IF EXISTS `crm_receivable_plan`;
 CREATE TABLE `crm_receivable_plan`  (
     `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
-    `index_no` bigint(20) NULL DEFAULT NULL COMMENT '期数',
+    `period` tinyint(4) DEFAULT NULL COMMENT '期数',
     `receivable_id` bigint(20) NULL DEFAULT NULL COMMENT '回款ID',
     `status` tinyint(4) NOT NULL COMMENT '完成状态',
     `check_status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批状态',
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
index 53572e2c4..b5ba20296 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
@@ -18,7 +18,7 @@ public class ReceivablePlanBaseVO {
 
     // TODO 芋艿:这个字段,在想想命名;
     @Schema(description = "期数")
-    private Long indexNo;
+    private Integer period;
 
     // TODO @liuhongfeng:中英文之间,有个空格,这样更干净;
     @Schema(description = "回款ID", example = "19852")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
index d08cfc6a7..0a3b2b7f8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
@@ -1,13 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
-import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
-import java.util.*;
+
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
 
 import com.alibaba.excel.annotation.ExcelProperty;
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
@@ -26,7 +22,7 @@ public class ReceivablePlanExcelVO {
     private Long id;
 
     @ExcelProperty("期数")
-    private Long indexNo;
+    private Integer period;
 
     @ExcelProperty("回款ID")
     private Long receivableId;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
index e68427d29..a9b505fe2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
@@ -1,9 +1,8 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
 import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
 import java.time.LocalDateTime;
 import org.springframework.format.annotation.DateTimeFormat;
 
@@ -14,7 +13,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 public class ReceivablePlanExportReqVO {
 
     @Schema(description = "期数")
-    private Long indexNo;
+    private Integer period;
 
     @Schema(description = "完成状态", example = "2")
     private Integer status;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
index acd4d5aa4..37c0446c3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
@@ -20,7 +20,7 @@ public class ReceivablePlanPageReqVO extends PageParam {
     // TODO 芋艿:筛选字段,需要去掉几个,在想想;
 
     @Schema(description = "期数")
-    private Long indexNo;
+    private Integer period;
 
     @Schema(description = "完成状态", example = "2")
     private Integer status;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
index 0c6f6e033..56ef67962 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
@@ -1,12 +1,10 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;
 
 import lombok.*;
-import java.util.*;
+
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
+
 import com.baomidou.mybatisplus.annotation.*;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 
@@ -33,7 +31,7 @@ public class ReceivablePlanDO extends BaseDO {
     /**
      * 期数
      */
-    private Long indexNo;
+    private Integer period;
     /**
      * 回款ID
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
index ac35133a7..3251c6787 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
@@ -19,7 +19,7 @@ public interface ReceivablePlanMapper extends BaseMapperX<ReceivablePlanDO> {
 
     default PageResult<ReceivablePlanDO> selectPage(ReceivablePlanPageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<ReceivablePlanDO>()
-                .eqIfPresent(ReceivablePlanDO::getIndexNo, reqVO.getIndexNo())
+                .eqIfPresent(ReceivablePlanDO::getPeriod, reqVO.getPeriod())
                 .eqIfPresent(ReceivablePlanDO::getStatus, reqVO.getStatus())
                 .eqIfPresent(ReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
                 .betweenIfPresent(ReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
@@ -35,7 +35,7 @@ public interface ReceivablePlanMapper extends BaseMapperX<ReceivablePlanDO> {
 
     default List<ReceivablePlanDO> selectList(ReceivablePlanExportReqVO reqVO) {
         return selectList(new LambdaQueryWrapperX<ReceivablePlanDO>()
-                .eqIfPresent(ReceivablePlanDO::getIndexNo, reqVO.getIndexNo())
+                .eqIfPresent(ReceivablePlanDO::getPeriod, reqVO.getPeriod())
                 .eqIfPresent(ReceivablePlanDO::getStatus, reqVO.getStatus())
                 .eqIfPresent(ReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
                 .betweenIfPresent(ReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
index 3739481c0..4f9fb8d26 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
@@ -107,7 +107,7 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
     public void testGetReceivablePlanPage() {
        // mock 数据
        ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class, o -> { // 等会查询到
-           o.setIndexNo(null);
+           o.setPeriod(null);
            o.setStatus(null);
            o.setCheckStatus(null);
            o.setReturnTime(null);
@@ -120,8 +120,8 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
            o.setCreateTime(null);
        });
        receivablePlanMapper.insert(dbReceivablePlan);
-       // 测试 indexNo 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setIndexNo(null)));
+       // 测试 Period 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setPeriod(null)));
        // 测试 status 不匹配
        receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
        // 测试 checkStatus 不匹配
@@ -144,7 +144,7 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
        receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
        // 准备参数
        ReceivablePlanPageReqVO reqVO = new ReceivablePlanPageReqVO();
-       reqVO.setIndexNo(null);
+       reqVO.setPeriod(null);
        reqVO.setStatus(null);
        reqVO.setCheckStatus(null);
        reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
@@ -169,7 +169,7 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
     public void testGetReceivablePlanList() {
        // mock 数据
        ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class, o -> { // 等会查询到
-           o.setIndexNo(null);
+           o.setPeriod(null);
            o.setStatus(null);
            o.setCheckStatus(null);
            o.setReturnTime(null);
@@ -182,8 +182,8 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
            o.setCreateTime(null);
        });
        receivablePlanMapper.insert(dbReceivablePlan);
-       // 测试 indexNo 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setIndexNo(null)));
+       // 测试 Period 不匹配
+       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setPeriod(null)));
        // 测试 status 不匹配
        receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
        // 测试 checkStatus 不匹配
@@ -206,7 +206,7 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
        receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
        // 准备参数
        ReceivablePlanExportReqVO reqVO = new ReceivablePlanExportReqVO();
-       reqVO.setIndexNo(null);
+       reqVO.setPeriod(null);
        reqVO.setStatus(null);
        reqVO.setCheckStatus(null);
        reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));

From f1f180ff77963f837acac390e94690cefcd05f95 Mon Sep 17 00:00:00 2001
From: liuhongfeng <291117974@qq.com>
Date: Tue, 31 Oct 2023 00:35:41 +0800
Subject: [PATCH 046/101] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91?=
 =?UTF-8?q?=E5=AD=97=E5=85=B8=E5=BD=92=E7=B1=BB=20=E3=80=90=E4=BF=AE?=
 =?UTF-8?q?=E6=94=B9=E3=80=91=E8=A7=84=E8=8C=83=E5=AE=9E=E4=BD=93=E5=91=BD?=
 =?UTF-8?q?=E5=90=8D=E5=92=8C=E5=AD=97=E6=AE=B5=E7=B1=BB=E5=9E=8B=E4=BF=AE?=
 =?UTF-8?q?=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/DictTypeConstants.java   |  1 +
 .../admin/receivable/vo/ReceivableBaseVO.java | 33 ++++---------------
 .../receivable/vo/ReceivableExcelVO.java      | 20 ++++-------
 .../receivable/vo/ReceivableExportReqVO.java  | 21 +++---------
 .../receivable/vo/ReceivablePageReqVO.java    | 24 ++------------
 .../receivable/vo/ReceivablePlanBaseVO.java   | 30 ++++++-----------
 .../receivable/vo/ReceivablePlanExcelVO.java  | 13 ++++----
 .../vo/ReceivablePlanExportReqVO.java         |  6 ++--
 .../vo/ReceivablePlanPageReqVO.java           | 15 ++-------
 .../dataobject/receivable/ReceivableDO.java   | 17 +++++-----
 .../receivable/ReceivablePlanDO.java          | 12 +++----
 .../mysql/receivable/ReceivableMapper.java    | 10 ------
 .../receivable/ReceivablePlanMapper.java      |  3 --
 .../receivable/ReceivableServiceImpl.java     |  9 ++++-
 .../ReceivablePlanServiceImplTest.java        |  3 --
 .../receivable/ReceivableServiceImplTest.java | 10 ------
 16 files changed, 64 insertions(+), 163 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
index 0e412ee9b..71f550775 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
@@ -11,5 +11,6 @@ public interface DictTypeConstants {
     String CRM_CUSTOMER_INDUSTRY = "crm_customer_industry"; // CRM 客户所属行业
     String CRM_CUSTOMER_LEVEL = "crm_customer_level"; // CRM 客户等级
     String CRM_CUSTOMER_SOURCE = "crm_customer_source"; // CRM 客户来源
+    String CRM_RECEIVABLE_CHECK_STATUS = "crm_receivable_check_status"; // CRM 审批状态
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
index 8aa43e50b..1e7e7bd68 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
@@ -19,30 +19,22 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class ReceivableBaseVO {
 
-    // TODO @liuhongfeng:部分缺少 example 的字段,要补充下;
-    // TODO @liuhongfeng:部分字段,需要必传,要写 requiredMode = Schema.RequiredMode.REQUIRED,以及对应的 validator 非空校验
-
-    @Schema(description = "回款编号")
+    @Schema(description = "回款编号",requiredMode = Schema.RequiredMode.REQUIRED, example = "31177")
     private String no;
 
-    // TODO @liuhongfeng:中英文之间,有个空格,这样更干净;
-    @Schema(description = "回款计划ID", example = "31177")
+    @Schema(description = "回款计划", example = "31177")
     private Long planId;
 
-    @Schema(description = "客户ID", example = "4963")
+    @Schema(description = "客户名称", example = "4963")
     private Long customerId;
 
-    @Schema(description = "合同ID", example = "30305")
+    @Schema(description = "合同名称", example = "30305")
     private Long contractId;
 
     @Schema(description = "审批状态", example = "1")
     @InEnum(AuditStatusEnum.class)
     private Integer checkStatus;
 
-    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
-    @Schema(description = "工作流编号", example = "16568")
-    private Long processInstanceId;
-
     @Schema(description = "回款日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime returnTime;
@@ -50,9 +42,8 @@ public class ReceivableBaseVO {
     @Schema(description = "回款方式", example = "2")
     private String returnType;
 
-    // TODO @liuhongfeng:使用 Int 哈,分;
     @Schema(description = "回款金额", example = "31859")
-    private BigDecimal price;
+    private Integer price;
 
     @Schema(description = "负责人", example = "22202")
     private Long ownerUserId;
@@ -63,19 +54,7 @@ public class ReceivableBaseVO {
     @Schema(description = "显示顺序")
     private Integer sort;
 
-    // TODO @芋艿:这个字段在看看;dataScope、dataScopeDeptIds
-    @Schema(description = "数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
-    private Integer dataScope;
-
-    @Schema(description = "数据范围(指定部门数组)")
-    private String dataScopeDeptIds;
-
-    // TODO @liuhongfeng:这个字段,这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
-    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    @NotNull(message = "状态不能为空")
-    private Integer status;
-
-    @Schema(description = "备注", example = "随便")
+    @Schema(description = "备注", example = "备注")
     private String remark;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
index b525341d3..a88fa9fd9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
+import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import java.util.*;
@@ -30,14 +31,14 @@ public class ReceivableExcelVO {
     @ExcelProperty("回款计划ID")
     private Long planId;
 
-    @ExcelProperty("客户ID")
+    @ExcelProperty("客户名称")
     private Long customerId;
 
-    @ExcelProperty("合同ID")
+    @ExcelProperty("合同名称")
     private Long contractId;
 
     @ExcelProperty(value = "审批状态", converter = DictConvert.class)
-    @DictFormat("crm_receivable_check_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_RECEIVABLE_CHECK_STATUS)
     private Integer checkStatus;
 
     @ExcelProperty("工作流编号")
@@ -50,7 +51,7 @@ public class ReceivableExcelVO {
     private String returnType;
 
     @ExcelProperty("回款金额")
-    private BigDecimal price;
+    private Integer price;
 
     @ExcelProperty("负责人")
     private Long ownerUserId;
@@ -58,17 +59,8 @@ public class ReceivableExcelVO {
     @ExcelProperty("批次")
     private Long batchId;
 
-    //@ExcelProperty("显示顺序")
-    //private Integer sort;
-
-    //@ExcelProperty("数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
-    //private Integer dataScope;
-
-    //@ExcelProperty("数据范围(指定部门数组)")
-    //private String dataScopeDeptIds;
-
     @ExcelProperty(value = "状态", converter = DictConvert.class)
-    @DictFormat("common_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    @DictFormat(DictTypeConstants.COMMON_STATUS)
     private Integer status;
 
     @ExcelProperty("备注")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
index 4a9a033f3..f07e4ddf8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
@@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -19,21 +18,18 @@ public class ReceivableExportReqVO {
     @Schema(description = "回款编号")
     private String no;
 
-    @Schema(description = "回款计划ID", example = "31177")
+    @Schema(description = "回款计划", example = "31177")
     private Long planId;
 
-    @Schema(description = "客户ID", example = "4963")
+    @Schema(description = "客户名称", example = "4963")
     private Long customerId;
 
-    @Schema(description = "合同ID", example = "30305")
+    @Schema(description = "合同名称", example = "30305")
     private Long contractId;
 
     @Schema(description = "审批状态", example = "1")
     private Integer checkStatus;
 
-    @Schema(description = "工作流编号", example = "16568")
-    private Long processInstanceId;
-
     @Schema(description = "回款日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] returnTime;
@@ -42,7 +38,7 @@ public class ReceivableExportReqVO {
     private String returnType;
 
     @Schema(description = "回款金额", example = "31859")
-    private BigDecimal price;
+    private Integer price;
 
     @Schema(description = "负责人", example = "22202")
     private Long ownerUserId;
@@ -50,15 +46,6 @@ public class ReceivableExportReqVO {
     @Schema(description = "批次", example = "2539")
     private Long batchId;
 
-    @Schema(description = "显示顺序")
-    private Integer sort;
-
-    @Schema(description = "数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
-    private Integer dataScope;
-
-    @Schema(description = "数据范围(指定部门数组)")
-    private String dataScopeDeptIds;
-
     @Schema(description = "状态", example = "1")
     private Integer status;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
index ba4775535..53cdc4949 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
@@ -18,25 +18,21 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class ReceivablePageReqVO extends PageParam {
 
-    // TODO @liuhongfeng:目前就使用 no 检索即可;
     @Schema(description = "回款编号")
     private String no;
 
     @Schema(description = "回款计划ID", example = "31177")
     private Long planId;
 
-    @Schema(description = "客户ID", example = "4963")
+    @Schema(description = "客户名称", example = "4963")
     private Long customerId;
 
-    @Schema(description = "合同ID", example = "30305")
+    @Schema(description = "合同名称", example = "30305")
     private Long contractId;
 
     @Schema(description = "审批状态", example = "1")
     private Integer checkStatus;
 
-    @Schema(description = "工作流编号", example = "16568")
-    private Long processInstanceId;
-
     @Schema(description = "回款日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] returnTime;
@@ -45,29 +41,15 @@ public class ReceivablePageReqVO extends PageParam {
     private String returnType;
 
     @Schema(description = "回款金额", example = "31859")
-    private BigDecimal price;
+    private Integer price;
 
     @Schema(description = "负责人", example = "22202")
     private Long ownerUserId;
 
-    @Schema(description = "批次", example = "2539")
-    private Long batchId;
-
-    @Schema(description = "显示顺序")
-    private Integer sort;
-
-    @Schema(description = "数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)")
-    private Integer dataScope;
-
-    @Schema(description = "数据范围(指定部门数组)")
-    private String dataScopeDeptIds;
 
     @Schema(description = "状态", example = "1")
     private Integer status;
 
-    @Schema(description = "备注", example = "随便")
-    private String remark;
-
     @Schema(description = "创建时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
index b5ba20296..2bd05a64a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
@@ -1,10 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.AuditStatusEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -16,57 +17,46 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class ReceivablePlanBaseVO {
 
-    // TODO 芋艿:这个字段,在想想命名;
-    @Schema(description = "期数")
+    @Schema(description = "期数", example = "1")
     private Integer period;
 
-    // TODO @liuhongfeng:中英文之间,有个空格,这样更干净;
-    @Schema(description = "回款ID", example = "19852")
+    @Schema(description = "回款计划", example = "19852")
     private Long receivableId;
 
     @Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    //@NotNull(message = "完成状态不能为空")
     private Integer status;
 
-    // TODO @liuhongfeng:这个字段,可以写个枚举,然后 InEnum 去校验下;
-    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的
     @Schema(description = "审批状态", example = "1")
+    @InEnum(AuditStatusEnum.class)
     private Integer checkStatus;
 
-    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
-    @Schema(description = "工作流编号", example = "8909")
-    private Long processInstanceId;
-
-    // TODO @liuhongfeng:使用 Int 哈,分;
     @Schema(description = "计划回款金额", example = "29675")
-    private BigDecimal price;
+    private Integer price;
 
     @Schema(description = "计划回款日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime returnTime;
 
-    // TODO @liuhongfeng:这个字段,Integer
     @Schema(description = "提前几天提醒")
-    private Long remindDays;
+    private Integer remindDays;
 
     @Schema(description = "提醒日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime remindTime;
 
-    @Schema(description = "客户ID", example = "18026")
+    @Schema(description = "客户名称", example = "18026")
     private Long customerId;
 
-    @Schema(description = "合同ID", example = "3473")
+    @Schema(description = "合同名称", example = "3473")
     private Long contractId;
 
-    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
     @Schema(description = "负责人", example = "17828")
     private Long ownerUserId;
 
     @Schema(description = "显示顺序")
     private Integer sort;
 
-    @Schema(description = "备注", example = "随便")
+    @Schema(description = "备注", example = "备注")
     private String remark;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
index e6646aa2d..2ff3fd0d3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
+import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
 import lombok.*;
 
 import java.math.BigDecimal;
@@ -27,25 +28,25 @@ public class ReceivablePlanExcelVO {
     @ExcelProperty("回款ID")
     private Long receivableId;
 
-    @ExcelProperty(value = "完成状态", converter = DictConvert.class)
-    @DictFormat("common_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.COMMON_STATUS)
     private Integer status;
 
     @ExcelProperty(value = "审批状态", converter = DictConvert.class)
-    @DictFormat("crm_receivable_check_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_RECEIVABLE_CHECK_STATUS)
     private Integer checkStatus;
 
     //@ExcelProperty("工作流编号")
     //private Long processInstanceId;
 
     @ExcelProperty("计划回款金额")
-    private BigDecimal price;
+    private Integer price;
 
     @ExcelProperty("计划回款日期")
     private LocalDateTime returnTime;
 
     @ExcelProperty("提前几天提醒")
-    private Long remindDays;
+    private Integer remindDays;
 
     @ExcelProperty("提醒日期")
     private LocalDateTime remindTime;
@@ -53,7 +54,7 @@ public class ReceivablePlanExcelVO {
     @ExcelProperty("客户ID")
     private Long customerId;
 
-    @ExcelProperty("合同ID")
+    @ExcelProperty("合同名称")
     private Long contractId;
 
     @ExcelProperty("负责人")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
index 694deb8fc..803e1ed03 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
@@ -26,16 +26,16 @@ public class ReceivablePlanExportReqVO {
     private LocalDateTime[] returnTime;
 
     @Schema(description = "提前几天提醒")
-    private Long remindDays;
+    private Integer remindDays;
 
     @Schema(description = "提醒日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] remindTime;
 
-    @Schema(description = "客户ID", example = "18026")
+    @Schema(description = "客户名称", example = "18026")
     private Long customerId;
 
-    @Schema(description = "合同ID", example = "3473")
+    @Schema(description = "合同名称", example = "3473")
     private Long contractId;
 
     @Schema(description = "负责人", example = "17828")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
index c445bee50..c92b4e891 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
@@ -17,11 +17,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class ReceivablePlanPageReqVO extends PageParam {
 
-    // TODO 芋艿:筛选字段,需要去掉几个,在想想;
-
-    @Schema(description = "期数")
-    private Integer period;
-
     @Schema(description = "完成状态", example = "2")
     private Integer status;
 
@@ -32,25 +27,19 @@ public class ReceivablePlanPageReqVO extends PageParam {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] returnTime;
 
-    @Schema(description = "提前几天提醒")
-    private Long remindDays;
-
     @Schema(description = "提醒日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] remindTime;
 
-    @Schema(description = "客户ID", example = "18026")
+    @Schema(description = "客户名称", example = "18026")
     private Long customerId;
 
-    @Schema(description = "合同ID", example = "3473")
+    @Schema(description = "合同名称", example = "3473")
     private Long contractId;
 
     @Schema(description = "负责人", example = "17828")
     private Long ownerUserId;
 
-    @Schema(description = "备注", example = "随便")
-    private String remark;
-
     @Schema(description = "创建时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
index d80f76a91..0cf2423de 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 /**
@@ -34,27 +33,26 @@ public class ReceivableDO extends BaseDO {
      */
     private String no;
     /**
-     * 回款计划ID
+     * 回款计划
      *
-     * TODO @liuhongfeng:这个字段,后续要写下关联的实体哈
+     * 对应实体 {@link cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO}
      */
     private Long planId;
     /**
      * 客户ID
      *
-     * TODO @liuhongfeng:这个字段,后续要写下关联的实体哈
+     * 对应实体 {@link cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO}
      */
     private Long customerId;
     /**
      * 合同ID
      *
-     * TODO @liuhongfeng:这个字段,后续要写下关联的实体哈
+     * 对应实体 {@link cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO}
      */
     private Long contractId;
     /**
      * 审批状态
-     *
-     * 枚举 {@link TODO crm_receivable_check_status 对应的类}
+     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_RECEIVABLE_CHECK_STATUS}
      */
     private Integer checkStatus;
     /**
@@ -74,7 +72,7 @@ public class ReceivableDO extends BaseDO {
     /**
      * 回款金额
      */
-    private BigDecimal price;
+    private Integer price;
     /**
      * 负责人
      */
@@ -99,7 +97,8 @@ public class ReceivableDO extends BaseDO {
     /**
      * 状态
      *
-     * 枚举 {@link TODO common_status 对应的类}
+     * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
+     *
      */
     private Integer status;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
index 56ef67962..caaee7211 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
@@ -37,15 +37,15 @@ public class ReceivablePlanDO extends BaseDO {
      */
     private Long receivableId;
     /**
-     * 完成状态
+     * 状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
      *
-     * 枚举 {@link TODO common_status 对应的类}
      */
     private Integer status;
     /**
      * 审批状态
-     *
-     * 枚举 {@link TODO crm_receivable_check_status 对应的类}
+     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_RECEIVABLE_CHECK_STATUS}
      */
     private Integer checkStatus;
     /**
@@ -55,7 +55,7 @@ public class ReceivablePlanDO extends BaseDO {
     /**
      * 计划回款金额
      */
-    private BigDecimal price;
+    private Integer price;
     /**
      * 计划回款日期
      */
@@ -63,7 +63,7 @@ public class ReceivablePlanDO extends BaseDO {
     /**
      * 提前几天提醒
      */
-    private Long remindDays;
+    private Integer remindDays;
     /**
      * 提醒日期
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
index 08031772c..0ef7165f4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
@@ -24,17 +24,11 @@ public interface ReceivableMapper extends BaseMapperX<ReceivableDO> {
                 .eqIfPresent(ReceivableDO::getCustomerId, reqVO.getCustomerId())
                 .eqIfPresent(ReceivableDO::getContractId, reqVO.getContractId())
                 .eqIfPresent(ReceivableDO::getCheckStatus, reqVO.getCheckStatus())
-                .eqIfPresent(ReceivableDO::getProcessInstanceId, reqVO.getProcessInstanceId())
                 .betweenIfPresent(ReceivableDO::getReturnTime, reqVO.getReturnTime())
                 .eqIfPresent(ReceivableDO::getReturnType, reqVO.getReturnType())
                 .eqIfPresent(ReceivableDO::getPrice, reqVO.getPrice())
                 .eqIfPresent(ReceivableDO::getOwnerUserId, reqVO.getOwnerUserId())
-                .eqIfPresent(ReceivableDO::getBatchId, reqVO.getBatchId())
-                .eqIfPresent(ReceivableDO::getSort, reqVO.getSort())
-                .eqIfPresent(ReceivableDO::getDataScope, reqVO.getDataScope())
-                .eqIfPresent(ReceivableDO::getDataScopeDeptIds, reqVO.getDataScopeDeptIds())
                 .eqIfPresent(ReceivableDO::getStatus, reqVO.getStatus())
-                .eqIfPresent(ReceivableDO::getRemark, reqVO.getRemark())
                 .betweenIfPresent(ReceivableDO::getCreateTime, reqVO.getCreateTime())
                 .orderByDesc(ReceivableDO::getId));
     }
@@ -46,15 +40,11 @@ public interface ReceivableMapper extends BaseMapperX<ReceivableDO> {
                 .eqIfPresent(ReceivableDO::getCustomerId, reqVO.getCustomerId())
                 .eqIfPresent(ReceivableDO::getContractId, reqVO.getContractId())
                 .eqIfPresent(ReceivableDO::getCheckStatus, reqVO.getCheckStatus())
-                .eqIfPresent(ReceivableDO::getProcessInstanceId, reqVO.getProcessInstanceId())
                 .betweenIfPresent(ReceivableDO::getReturnTime, reqVO.getReturnTime())
                 .eqIfPresent(ReceivableDO::getReturnType, reqVO.getReturnType())
                 .eqIfPresent(ReceivableDO::getPrice, reqVO.getPrice())
                 .eqIfPresent(ReceivableDO::getOwnerUserId, reqVO.getOwnerUserId())
                 .eqIfPresent(ReceivableDO::getBatchId, reqVO.getBatchId())
-                .eqIfPresent(ReceivableDO::getSort, reqVO.getSort())
-                .eqIfPresent(ReceivableDO::getDataScope, reqVO.getDataScope())
-                .eqIfPresent(ReceivableDO::getDataScopeDeptIds, reqVO.getDataScopeDeptIds())
                 .eqIfPresent(ReceivableDO::getStatus, reqVO.getStatus())
                 .eqIfPresent(ReceivableDO::getRemark, reqVO.getRemark())
                 .betweenIfPresent(ReceivableDO::getCreateTime, reqVO.getCreateTime())
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
index 3251c6787..516f20c0c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
@@ -19,16 +19,13 @@ public interface ReceivablePlanMapper extends BaseMapperX<ReceivablePlanDO> {
 
     default PageResult<ReceivablePlanDO> selectPage(ReceivablePlanPageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<ReceivablePlanDO>()
-                .eqIfPresent(ReceivablePlanDO::getPeriod, reqVO.getPeriod())
                 .eqIfPresent(ReceivablePlanDO::getStatus, reqVO.getStatus())
                 .eqIfPresent(ReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
                 .betweenIfPresent(ReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
-                .eqIfPresent(ReceivablePlanDO::getRemindDays, reqVO.getRemindDays())
                 .betweenIfPresent(ReceivablePlanDO::getRemindTime, reqVO.getRemindTime())
                 .eqIfPresent(ReceivablePlanDO::getCustomerId, reqVO.getCustomerId())
                 .eqIfPresent(ReceivablePlanDO::getContractId, reqVO.getContractId())
                 .eqIfPresent(ReceivablePlanDO::getOwnerUserId, reqVO.getOwnerUserId())
-                .eqIfPresent(ReceivablePlanDO::getRemark, reqVO.getRemark())
                 .betweenIfPresent(ReceivablePlanDO::getCreateTime, reqVO.getCreateTime())
                 .orderByDesc(ReceivablePlanDO::getId));
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
index cf216d4d2..d3c7a16ef 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
@@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
 import cn.iocoder.yudao.module.crm.enums.AuditStatusEnum;
 import cn.iocoder.yudao.module.crm.service.contract.ContractService;
@@ -43,10 +44,11 @@ public class ReceivableServiceImpl implements ReceivableService {
     private ContractService contractService;
     @Resource
     private CrmCustomerService crmCustomerService;
+    @Resource
+    private ReceivablePlanService receivablePlanService;
 
     @Override
     public Long createReceivable(ReceivableCreateReqVO createReqVO) {
-        // TODO @liuhongfeng:planId 是否存在,是否合法,需要去校验;
         // 插入
         ReceivableDO receivable = ReceivableConvert.INSTANCE.convert(createReqVO);
         if (ObjectUtil.isNull(receivable.getStatus())){
@@ -80,6 +82,11 @@ public class ReceivableServiceImpl implements ReceivableService {
             throw exception(CUSTOMER_NOT_EXISTS);
         }
 
+        ReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(receivable.getPlanId());
+        if(ObjectUtil.isNull(receivablePlan)){
+            throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
+        }
+
     }
 
     @Override
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
index 4f9fb8d26..952514ecb 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
@@ -144,16 +144,13 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
        receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
        // 准备参数
        ReceivablePlanPageReqVO reqVO = new ReceivablePlanPageReqVO();
-       reqVO.setPeriod(null);
        reqVO.setStatus(null);
        reqVO.setCheckStatus(null);
        reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-       reqVO.setRemindDays(null);
        reqVO.setRemindTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
        reqVO.setCustomerId(null);
        reqVO.setContractId(null);
        reqVO.setOwnerUserId(null);
-       reqVO.setRemark(null);
        reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
 
        // 调用
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
index b8d4018b4..906fdb436 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
@@ -167,17 +167,11 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
        reqVO.setCustomerId(null);
        reqVO.setContractId(null);
        reqVO.setCheckStatus(null);
-       reqVO.setProcessInstanceId(null);
        reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
        reqVO.setReturnType(null);
        reqVO.setPrice(null);
        reqVO.setOwnerUserId(null);
-       reqVO.setBatchId(null);
-       reqVO.setSort(null);
-       reqVO.setDataScope(null);
-       reqVO.setDataScopeDeptIds(null);
        reqVO.setStatus(null);
-       reqVO.setRemark(null);
        reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
 
        // 调用
@@ -253,15 +247,11 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
        reqVO.setCustomerId(null);
        reqVO.setContractId(null);
        reqVO.setCheckStatus(null);
-       reqVO.setProcessInstanceId(null);
        reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
        reqVO.setReturnType(null);
        reqVO.setPrice(null);
        reqVO.setOwnerUserId(null);
        reqVO.setBatchId(null);
-       reqVO.setSort(null);
-       reqVO.setDataScope(null);
-       reqVO.setDataScopeDeptIds(null);
        reqVO.setStatus(null);
        reqVO.setRemark(null);
        reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));

From 95e9dc81c9c397078a8684dfbddc6599436f76fa Mon Sep 17 00:00:00 2001
From: liuhongfeng <291117974@qq.com>
Date: Tue, 31 Oct 2023 01:09:40 +0800
Subject: [PATCH 047/101] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E3=80=91?=
 =?UTF-8?q?=E5=9B=9E=E6=AC=BE=E8=AE=A1=E5=88=92=E5=92=8C=E5=9B=9E=E6=AC=BE?=
 =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=BB=9F=E4=B8=80=E5=B8=A6=E4=B8=8ACrm?=
 =?UTF-8?q?=E5=89=8D=E7=BC=80=EF=BC=8C=E9=98=B2=E6=AD=A2=E5=92=8C=E5=85=B6?=
 =?UTF-8?q?=E4=BB=96=E6=A8=A1=E5=9D=97=E5=86=B2=E7=AA=81=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ...ller.java => CrmReceivableController.java} |  40 +++---
 ....java => CrmReceivablePlanController.java} |  40 +++---
 ...leBaseVO.java => CrmReceivableBaseVO.java} |   7 +-
 ...qVO.java => CrmReceivableCreateReqVO.java} |   4 +-
 ...ExcelVO.java => CrmReceivableExcelVO.java} |   8 +-
 ...qVO.java => CrmReceivableExportReqVO.java} |   4 +-
 ...ReqVO.java => CrmReceivablePageReqVO.java} |   3 +-
 ...seVO.java => CrmReceivablePlanBaseVO.java} |   2 +-
 ...java => CrmReceivablePlanCreateReqVO.java} |   4 +-
 ...lVO.java => CrmReceivablePlanExcelVO.java} |   3 +-
 ...java => CrmReceivablePlanExportReqVO.java} |   4 +-
 ...O.java => CrmReceivablePlanPageReqVO.java} |   2 +-
 ...spVO.java => CrmReceivablePlanRespVO.java} |   2 +-
 ...java => CrmReceivablePlanUpdateReqVO.java} |   4 +-
 ...leRespVO.java => CrmReceivableRespVO.java} |   2 +-
 ...qVO.java => CrmReceivableUpdateReqVO.java} |   4 +-
 .../receivable/CrmReceivableConvert.java      |  34 +++++
 .../receivable/CrmReceivablePlanConvert.java  |  34 +++++
 .../convert/receivable/ReceivableConvert.java |  34 -----
 .../receivable/ReceivablePlanConvert.java     |  34 -----
 ...ReceivableDO.java => CrmReceivableDO.java} |   4 +-
 ...lePlanDO.java => CrmReceivablePlanDO.java} |   3 +-
 .../mysql/receivable/CrmReceivableMapper.java |  54 ++++++++
 .../receivable/CrmReceivablePlanMapper.java   |  49 +++++++
 .../mysql/receivable/ReceivableMapper.java    |  54 --------
 .../receivable/ReceivablePlanMapper.java      |  49 -------
 ...ice.java => CrmReceivablePlanService.java} |  16 +--
 ...java => CrmReceivablePlanServiceImpl.java} |  53 ++++----
 ...Service.java => CrmReceivableService.java} |  16 +--
 ...mpl.java => CrmReceivableServiceImpl.java} |  59 ++++----
 ...ableMapper.xml => CrmReceivableMapper.xml} |   2 +-
 ...Mapper.xml => CrmReceivablePlanMapper.xml} |   2 +-
 ... CrmCrmReceivablePlanServiceImplTest.java} | 102 +++++++-------
 ...a => CrmCrmReceivableServiceImplTest.java} | 126 +++++++++---------
 34 files changed, 423 insertions(+), 435 deletions(-)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/{ReceivableController.java => CrmReceivableController.java} (64%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/{ReceivablePlanController.java => CrmReceivablePlanController.java} (62%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivableBaseVO.java => CrmReceivableBaseVO.java} (91%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivableCreateReqVO.java => CrmReceivableCreateReqVO.java} (69%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivableExcelVO.java => CrmReceivableExcelVO.java} (89%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivableExportReqVO.java => CrmReceivableExportReqVO.java} (94%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivablePageReqVO.java => CrmReceivablePageReqVO.java} (95%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivablePlanBaseVO.java => CrmReceivablePlanBaseVO.java} (98%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivablePlanCreateReqVO.java => CrmReceivablePlanCreateReqVO.java} (68%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivablePlanExcelVO.java => CrmReceivablePlanExcelVO.java} (96%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivablePlanExportReqVO.java => CrmReceivablePlanExportReqVO.java} (93%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivablePlanPageReqVO.java => CrmReceivablePlanPageReqVO.java} (96%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivablePlanRespVO.java => CrmReceivablePlanRespVO.java} (88%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivablePlanUpdateReqVO.java => CrmReceivablePlanUpdateReqVO.java} (84%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivableRespVO.java => CrmReceivableRespVO.java} (89%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/{ReceivableUpdateReqVO.java => CrmReceivableUpdateReqVO.java} (85%)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivableConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivablePlanConvert.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivableConvert.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivablePlanConvert.java
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/{ReceivableDO.java => CrmReceivableDO.java} (94%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/{ReceivablePlanDO.java => CrmReceivablePlanDO.java} (95%)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/{ReceivablePlanService.java => CrmReceivablePlanService.java} (64%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/{ReceivablePlanServiceImpl.java => CrmReceivablePlanServiceImpl.java} (63%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/{ReceivableService.java => CrmReceivableService.java} (67%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/{ReceivableServiceImpl.java => CrmReceivableServiceImpl.java} (62%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/{ReceivableMapper.xml => CrmReceivableMapper.xml} (95%)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/{ReceivablePlanMapper.xml => CrmReceivablePlanMapper.xml} (95%)
 rename yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/{ReceivablePlanServiceImplTest.java => CrmCrmReceivablePlanServiceImplTest.java} (58%)
 rename yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/{ReceivableServiceImplTest.java => CrmCrmReceivableServiceImplTest.java} (57%)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
similarity index 64%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
index c88f31f6d..4f7d9e674 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivableController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
@@ -5,9 +5,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.*;
-import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
-import cn.iocoder.yudao.module.crm.service.receivable.ReceivableService;
+import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivableConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
+import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -28,23 +28,23 @@ import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.E
 @RestController
 @RequestMapping("/crm/receivable")
 @Validated
-public class ReceivableController {
+public class CrmReceivableController {
 
     @Resource
-    private ReceivableService receivableService;
+    private CrmReceivableService crmReceivableService;
 
     @PostMapping("/create")
     @Operation(summary = "创建回款管理")
     @PreAuthorize("@ss.hasPermission('crm:receivable:create')")
-    public CommonResult<Long> createReceivable(@Valid @RequestBody ReceivableCreateReqVO createReqVO) {
-        return success(receivableService.createReceivable(createReqVO));
+    public CommonResult<Long> createReceivable(@Valid @RequestBody CrmReceivableCreateReqVO createReqVO) {
+        return success(crmReceivableService.createReceivable(createReqVO));
     }
 
     @PutMapping("/update")
     @Operation(summary = "更新回款管理")
     @PreAuthorize("@ss.hasPermission('crm:receivable:update')")
-    public CommonResult<Boolean> updateReceivable(@Valid @RequestBody ReceivableUpdateReqVO updateReqVO) {
-        receivableService.updateReceivable(updateReqVO);
+    public CommonResult<Boolean> updateReceivable(@Valid @RequestBody CrmReceivableUpdateReqVO updateReqVO) {
+        crmReceivableService.updateReceivable(updateReqVO);
         return success(true);
     }
 
@@ -53,7 +53,7 @@ public class ReceivableController {
     @Parameter(name = "id", description = "编号", required = true)
     @PreAuthorize("@ss.hasPermission('crm:receivable:delete')")
     public CommonResult<Boolean> deleteReceivable(@RequestParam("id") Long id) {
-        receivableService.deleteReceivable(id);
+        crmReceivableService.deleteReceivable(id);
         return success(true);
     }
 
@@ -61,29 +61,29 @@ public class ReceivableController {
     @Operation(summary = "获得回款管理")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
-    public CommonResult<ReceivableRespVO> getReceivable(@RequestParam("id") Long id) {
-        ReceivableDO receivable = receivableService.getReceivable(id);
-        return success(ReceivableConvert.INSTANCE.convert(receivable));
+    public CommonResult<CrmReceivableRespVO> getReceivable(@RequestParam("id") Long id) {
+        CrmReceivableDO receivable = crmReceivableService.getReceivable(id);
+        return success(CrmReceivableConvert.INSTANCE.convert(receivable));
     }
 
     @GetMapping("/page")
     @Operation(summary = "获得回款管理分页")
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
-    public CommonResult<PageResult<ReceivableRespVO>> getReceivablePage(@Valid ReceivablePageReqVO pageVO) {
-        PageResult<ReceivableDO> pageResult = receivableService.getReceivablePage(pageVO);
-        return success(ReceivableConvert.INSTANCE.convertPage(pageResult));
+    public CommonResult<PageResult<CrmReceivableRespVO>> getReceivablePage(@Valid CrmReceivablePageReqVO pageVO) {
+        PageResult<CrmReceivableDO> pageResult = crmReceivableService.getReceivablePage(pageVO);
+        return success(CrmReceivableConvert.INSTANCE.convertPage(pageResult));
     }
 
     @GetMapping("/export-excel")
     @Operation(summary = "导出回款管理 Excel")
     @PreAuthorize("@ss.hasPermission('crm:receivable:export')")
     @OperateLog(type = EXPORT)
-    public void exportReceivableExcel(@Valid ReceivableExportReqVO exportReqVO,
+    public void exportReceivableExcel(@Valid CrmReceivableExportReqVO exportReqVO,
               HttpServletResponse response) throws IOException {
-        List<ReceivableDO> list = receivableService.getReceivableList(exportReqVO);
+        List<CrmReceivableDO> list = crmReceivableService.getReceivableList(exportReqVO);
         // 导出 Excel
-        List<ReceivableExcelVO> datas = ReceivableConvert.INSTANCE.convertList02(list);
-        ExcelUtils.write(response, "回款管理.xls", "数据", ReceivableExcelVO.class, datas);
+        List<CrmReceivableExcelVO> datas = CrmReceivableConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "回款管理.xls", "数据", CrmReceivableExcelVO.class, datas);
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
similarity index 62%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
index 0c33eb25a..afba2b721 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/ReceivablePlanController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
@@ -5,9 +5,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.*;
-import cn.iocoder.yudao.module.crm.convert.receivable.ReceivablePlanConvert;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
-import cn.iocoder.yudao.module.crm.service.receivable.ReceivablePlanService;
+import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivablePlanService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -28,23 +28,23 @@ import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.E
 @RestController
 @RequestMapping("/crm/receivable-plan")
 @Validated
-public class ReceivablePlanController {
+public class CrmReceivablePlanController {
 
     @Resource
-    private ReceivablePlanService receivablePlanService;
+    private CrmReceivablePlanService crmReceivablePlanService;
 
     @PostMapping("/create")
     @Operation(summary = "创建回款计划")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:create')")
-    public CommonResult<Long> createReceivablePlan(@Valid @RequestBody ReceivablePlanCreateReqVO createReqVO) {
-        return success(receivablePlanService.createReceivablePlan(createReqVO));
+    public CommonResult<Long> createReceivablePlan(@Valid @RequestBody CrmReceivablePlanCreateReqVO createReqVO) {
+        return success(crmReceivablePlanService.createReceivablePlan(createReqVO));
     }
 
     @PutMapping("/update")
     @Operation(summary = "更新回款计划")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:update')")
-    public CommonResult<Boolean> updateReceivablePlan(@Valid @RequestBody ReceivablePlanUpdateReqVO updateReqVO) {
-        receivablePlanService.updateReceivablePlan(updateReqVO);
+    public CommonResult<Boolean> updateReceivablePlan(@Valid @RequestBody CrmReceivablePlanUpdateReqVO updateReqVO) {
+        crmReceivablePlanService.updateReceivablePlan(updateReqVO);
         return success(true);
     }
 
@@ -53,7 +53,7 @@ public class ReceivablePlanController {
     @Parameter(name = "id", description = "编号", required = true)
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:delete')")
     public CommonResult<Boolean> deleteReceivablePlan(@RequestParam("id") Long id) {
-        receivablePlanService.deleteReceivablePlan(id);
+        crmReceivablePlanService.deleteReceivablePlan(id);
         return success(true);
     }
 
@@ -61,29 +61,29 @@ public class ReceivablePlanController {
     @Operation(summary = "获得回款计划")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
-    public CommonResult<ReceivablePlanRespVO> getReceivablePlan(@RequestParam("id") Long id) {
-        ReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(id);
-        return success(ReceivablePlanConvert.INSTANCE.convert(receivablePlan));
+    public CommonResult<CrmReceivablePlanRespVO> getReceivablePlan(@RequestParam("id") Long id) {
+        CrmReceivablePlanDO receivablePlan = crmReceivablePlanService.getReceivablePlan(id);
+        return success(CrmReceivablePlanConvert.INSTANCE.convert(receivablePlan));
     }
 
     @GetMapping("/page")
     @Operation(summary = "获得回款计划分页")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
-    public CommonResult<PageResult<ReceivablePlanRespVO>> getReceivablePlanPage(@Valid ReceivablePlanPageReqVO pageVO) {
-        PageResult<ReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(pageVO);
-        return success(ReceivablePlanConvert.INSTANCE.convertPage(pageResult));
+    public CommonResult<PageResult<CrmReceivablePlanRespVO>> getReceivablePlanPage(@Valid CrmReceivablePlanPageReqVO pageVO) {
+        PageResult<CrmReceivablePlanDO> pageResult = crmReceivablePlanService.getReceivablePlanPage(pageVO);
+        return success(CrmReceivablePlanConvert.INSTANCE.convertPage(pageResult));
     }
 
     @GetMapping("/export-excel")
     @Operation(summary = "导出回款计划 Excel")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:export')")
     @OperateLog(type = EXPORT)
-    public void exportReceivablePlanExcel(@Valid ReceivablePlanExportReqVO exportReqVO,
+    public void exportReceivablePlanExcel(@Valid CrmReceivablePlanExportReqVO exportReqVO,
               HttpServletResponse response) throws IOException {
-        List<ReceivablePlanDO> list = receivablePlanService.getReceivablePlanList(exportReqVO);
+        List<CrmReceivablePlanDO> list = crmReceivablePlanService.getReceivablePlanList(exportReqVO);
         // 导出 Excel
-        List<ReceivablePlanExcelVO> datas = ReceivablePlanConvert.INSTANCE.convertList02(list);
-        ExcelUtils.write(response, "回款计划.xls", "数据", ReceivablePlanExcelVO.class, datas);
+        List<CrmReceivablePlanExcelVO> datas = CrmReceivablePlanConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "回款计划.xls", "数据", CrmReceivablePlanExcelVO.class, datas);
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableBaseVO.java
similarity index 91%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableBaseVO.java
index 1e7e7bd68..24b1d57e4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableBaseVO.java
@@ -6,8 +6,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
-import javax.validation.constraints.NotNull;
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -17,7 +15,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
  * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
  */
 @Data
-public class ReceivableBaseVO {
+public class CrmReceivableBaseVO {
 
     @Schema(description = "回款编号",requiredMode = Schema.RequiredMode.REQUIRED, example = "31177")
     private String no;
@@ -57,4 +55,7 @@ public class ReceivableBaseVO {
     @Schema(description = "备注", example = "备注")
     private String remark;
 
+    @Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer status;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableCreateReqVO.java
similarity index 69%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableCreateReqVO.java
index d38d0bd5f..a2d43d8c1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableCreateReqVO.java
@@ -1,14 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
 import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import javax.validation.constraints.*;
 
 @Schema(description = "管理后台 - CRM 回款创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ReceivableCreateReqVO extends ReceivableBaseVO {
+public class CrmReceivableCreateReqVO extends CrmReceivableBaseVO {
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java
similarity index 89%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java
index a88fa9fd9..1bd461499 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java
@@ -1,12 +1,8 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
-import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
+
 import java.time.LocalDateTime;
 
 import com.alibaba.excel.annotation.ExcelProperty;
@@ -20,7 +16,7 @@ import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  * @author 赤焰
  */
 @Data
-public class ReceivableExcelVO {
+public class CrmReceivableExcelVO {
 
     @ExcelProperty("ID")
     private Long id;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java
similarity index 94%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java
index f07e4ddf8..b674bbc2b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java
@@ -11,9 +11,9 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 /**
  * @author 赤焰
  */
-@Schema(description = "管理后台 - CRM 回款 Excel 导出 Request VO,参数和 ReceivablePageReqVO 是一致的")
+@Schema(description = "管理后台 - CRM 回款 Excel 导出 Request VO,参数和 CrmReceivablePageReqVO 是一致的")
 @Data
-public class ReceivableExportReqVO {
+public class CrmReceivableExportReqVO {
 
     @Schema(description = "回款编号")
     private String no;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java
similarity index 95%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java
index 53cdc4949..3a90ed809 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java
@@ -7,7 +7,6 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 import org.springframework.format.annotation.DateTimeFormat;
 
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -16,7 +15,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ReceivablePageReqVO extends PageParam {
+public class CrmReceivablePageReqVO extends PageParam {
 
     @Schema(description = "回款编号")
     private String no;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanBaseVO.java
similarity index 98%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanBaseVO.java
index 2bd05a64a..49d00892e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanBaseVO.java
@@ -15,7 +15,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
  * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
  */
 @Data
-public class ReceivablePlanBaseVO {
+public class CrmReceivablePlanBaseVO {
 
     @Schema(description = "期数", example = "1")
     private Integer period;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanCreateReqVO.java
similarity index 68%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanCreateReqVO.java
index d03e76eb0..cebfd28cb 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanCreateReqVO.java
@@ -1,14 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
 import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import javax.validation.constraints.*;
 
 @Schema(description = "管理后台 - CRM 回款计划创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ReceivablePlanCreateReqVO extends ReceivablePlanBaseVO {
+public class CrmReceivablePlanCreateReqVO extends CrmReceivablePlanBaseVO {
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java
similarity index 96%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java
index 2ff3fd0d3..a7246e252 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
 import lombok.*;
 
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 import com.alibaba.excel.annotation.ExcelProperty;
@@ -17,7 +16,7 @@ import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  * @author 芋道源码
  */
 @Data
-public class ReceivablePlanExcelVO {
+public class CrmReceivablePlanExcelVO {
 
     @ExcelProperty("ID")
     private Long id;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java
similarity index 93%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java
index 803e1ed03..8002d41af 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java
@@ -8,9 +8,9 @@ import org.springframework.format.annotation.DateTimeFormat;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - CRM 回款计划 Excel 导出 Request VO,参数和 ReceivablePlanPageReqVO 是一致的")
+@Schema(description = "管理后台 - CRM 回款计划 Excel 导出 Request VO,参数和 CrmReceivablePlanPageReqVO 是一致的")
 @Data
-public class ReceivablePlanExportReqVO {
+public class CrmReceivablePlanExportReqVO {
 
     @Schema(description = "期数")
     private Integer period;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanPageReqVO.java
similarity index 96%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanPageReqVO.java
index c92b4e891..10e26207a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanPageReqVO.java
@@ -15,7 +15,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ReceivablePlanPageReqVO extends PageParam {
+public class CrmReceivablePlanPageReqVO extends PageParam {
 
     @Schema(description = "完成状态", example = "2")
     private Integer status;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanRespVO.java
similarity index 88%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanRespVO.java
index ce49d5977..243e7f782 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanRespVO.java
@@ -8,7 +8,7 @@ import java.time.LocalDateTime;
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ReceivablePlanRespVO extends ReceivablePlanBaseVO {
+public class CrmReceivablePlanRespVO extends CrmReceivablePlanBaseVO {
 
     @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25153")
     private Long id;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanUpdateReqVO.java
similarity index 84%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanUpdateReqVO.java
index 1f539537d..5471cfba3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivablePlanUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanUpdateReqVO.java
@@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
-import java.util.*;
+
 import javax.validation.constraints.*;
 
 @Schema(description = "管理后台 - CRM 回款计划更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ReceivablePlanUpdateReqVO extends ReceivablePlanBaseVO {
+public class CrmReceivablePlanUpdateReqVO extends CrmReceivablePlanBaseVO {
 
     @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25153")
     @NotNull(message = "ID不能为空")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableRespVO.java
similarity index 89%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableRespVO.java
index 1646cd5a8..00d984da5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableRespVO.java
@@ -8,7 +8,7 @@ import java.time.LocalDateTime;
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ReceivableRespVO extends ReceivableBaseVO {
+public class CrmReceivableRespVO extends CrmReceivableBaseVO {
 
     @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
     private Long id;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableUpdateReqVO.java
similarity index 85%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableUpdateReqVO.java
index 008c06b63..d6241f258 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/ReceivableUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableUpdateReqVO.java
@@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
-import java.util.*;
+
 import javax.validation.constraints.*;
 
 @Schema(description = "管理后台 - CRM 回款更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ReceivableUpdateReqVO extends ReceivableBaseVO {
+public class CrmReceivableUpdateReqVO extends CrmReceivableBaseVO {
 
     @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
     @NotNull(message = "ID不能为空")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivableConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivableConvert.java
new file mode 100644
index 000000000..23345fbe6
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivableConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.receivable;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+
+/**
+ * 回款管理 Convert
+ *
+ * @author 赤焰
+ */
+@Mapper
+public interface CrmReceivableConvert {
+
+    CrmReceivableConvert INSTANCE = Mappers.getMapper(CrmReceivableConvert.class);
+
+    CrmReceivableDO convert(CrmReceivableCreateReqVO bean);
+
+    CrmReceivableDO convert(CrmReceivableUpdateReqVO bean);
+
+    CrmReceivableRespVO convert(CrmReceivableDO bean);
+
+    List<CrmReceivableRespVO> convertList(List<CrmReceivableDO> list);
+
+    PageResult<CrmReceivableRespVO> convertPage(PageResult<CrmReceivableDO> page);
+
+    List<CrmReceivableExcelVO> convertList02(List<CrmReceivableDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivablePlanConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivablePlanConvert.java
new file mode 100644
index 000000000..4b2d65c60
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivablePlanConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.receivable;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+
+/**
+ * 回款计划 Convert
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface CrmReceivablePlanConvert {
+
+    CrmReceivablePlanConvert INSTANCE = Mappers.getMapper(CrmReceivablePlanConvert.class);
+
+    CrmReceivablePlanDO convert(CrmReceivablePlanCreateReqVO bean);
+
+    CrmReceivablePlanDO convert(CrmReceivablePlanUpdateReqVO bean);
+
+    CrmReceivablePlanRespVO convert(CrmReceivablePlanDO bean);
+
+    List<CrmReceivablePlanRespVO> convertList(List<CrmReceivablePlanDO> list);
+
+    PageResult<CrmReceivablePlanRespVO> convertPage(PageResult<CrmReceivablePlanDO> page);
+
+    List<CrmReceivablePlanExcelVO> convertList02(List<CrmReceivablePlanDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivableConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivableConvert.java
deleted file mode 100644
index 7f14aaddd..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivableConvert.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package cn.iocoder.yudao.module.crm.convert.receivable;
-
-import java.util.*;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
-
-/**
- * 回款管理 Convert
- *
- * @author 赤焰
- */
-@Mapper
-public interface ReceivableConvert {
-
-    ReceivableConvert INSTANCE = Mappers.getMapper(ReceivableConvert.class);
-
-    ReceivableDO convert(ReceivableCreateReqVO bean);
-
-    ReceivableDO convert(ReceivableUpdateReqVO bean);
-
-    ReceivableRespVO convert(ReceivableDO bean);
-
-    List<ReceivableRespVO> convertList(List<ReceivableDO> list);
-
-    PageResult<ReceivableRespVO> convertPage(PageResult<ReceivableDO> page);
-
-    List<ReceivableExcelVO> convertList02(List<ReceivableDO> list);
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivablePlanConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivablePlanConvert.java
deleted file mode 100644
index 81ec646f3..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/ReceivablePlanConvert.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package cn.iocoder.yudao.module.crm.convert.receivable;
-
-import java.util.*;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
-
-/**
- * 回款计划 Convert
- *
- * @author 芋道源码
- */
-@Mapper
-public interface ReceivablePlanConvert {
-
-    ReceivablePlanConvert INSTANCE = Mappers.getMapper(ReceivablePlanConvert.class);
-
-    ReceivablePlanDO convert(ReceivablePlanCreateReqVO bean);
-
-    ReceivablePlanDO convert(ReceivablePlanUpdateReqVO bean);
-
-    ReceivablePlanRespVO convert(ReceivablePlanDO bean);
-
-    List<ReceivablePlanRespVO> convertList(List<ReceivablePlanDO> list);
-
-    PageResult<ReceivablePlanRespVO> convertPage(PageResult<ReceivablePlanDO> page);
-
-    List<ReceivablePlanExcelVO> convertList02(List<ReceivablePlanDO> list);
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
similarity index 94%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
index 0cf2423de..b74507dd1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivableDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
@@ -21,7 +21,7 @@ import java.time.LocalDateTime;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class ReceivableDO extends BaseDO {
+public class CrmReceivableDO extends BaseDO {
 
     /**
      * ID
@@ -35,7 +35,7 @@ public class ReceivableDO extends BaseDO {
     /**
      * 回款计划
      *
-     * 对应实体 {@link cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO}
+     * 对应实体 {@link CrmReceivablePlanDO}
      */
     private Long planId;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java
similarity index 95%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java
index caaee7211..4274250e8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/ReceivablePlanDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;
 
 import lombok.*;
 
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 import com.baomidou.mybatisplus.annotation.*;
@@ -21,7 +20,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class ReceivablePlanDO extends BaseDO {
+public class CrmReceivablePlanDO extends BaseDO {
 
     /**
      * ID
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
new file mode 100644
index 000000000..716704fb8
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+
+/**
+ * 回款管理 Mapper
+ *
+ * @author 赤焰
+ */
+@Mapper
+public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
+
+    default PageResult<CrmReceivableDO> selectPage(CrmReceivablePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmReceivableDO>()
+                .eqIfPresent(CrmReceivableDO::getNo, reqVO.getNo())
+                .eqIfPresent(CrmReceivableDO::getPlanId, reqVO.getPlanId())
+                .eqIfPresent(CrmReceivableDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(CrmReceivableDO::getContractId, reqVO.getContractId())
+                .eqIfPresent(CrmReceivableDO::getCheckStatus, reqVO.getCheckStatus())
+                .betweenIfPresent(CrmReceivableDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(CrmReceivableDO::getReturnType, reqVO.getReturnType())
+                .eqIfPresent(CrmReceivableDO::getPrice, reqVO.getPrice())
+                .eqIfPresent(CrmReceivableDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .eqIfPresent(CrmReceivableDO::getStatus, reqVO.getStatus())
+                .betweenIfPresent(CrmReceivableDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmReceivableDO::getId));
+    }
+
+    default List<CrmReceivableDO> selectList(CrmReceivableExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CrmReceivableDO>()
+                .eqIfPresent(CrmReceivableDO::getNo, reqVO.getNo())
+                .eqIfPresent(CrmReceivableDO::getPlanId, reqVO.getPlanId())
+                .eqIfPresent(CrmReceivableDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(CrmReceivableDO::getContractId, reqVO.getContractId())
+                .eqIfPresent(CrmReceivableDO::getCheckStatus, reqVO.getCheckStatus())
+                .betweenIfPresent(CrmReceivableDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(CrmReceivableDO::getReturnType, reqVO.getReturnType())
+                .eqIfPresent(CrmReceivableDO::getPrice, reqVO.getPrice())
+                .eqIfPresent(CrmReceivableDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .eqIfPresent(CrmReceivableDO::getBatchId, reqVO.getBatchId())
+                .eqIfPresent(CrmReceivableDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(CrmReceivableDO::getRemark, reqVO.getRemark())
+                .betweenIfPresent(CrmReceivableDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmReceivableDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java
new file mode 100644
index 000000000..62b2c0c54
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
+
+/**
+ * 回款计划 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO> {
+
+    default PageResult<CrmReceivablePlanDO> selectPage(CrmReceivablePlanPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmReceivablePlanDO>()
+                .eqIfPresent(CrmReceivablePlanDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(CrmReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
+                .betweenIfPresent(CrmReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
+                .betweenIfPresent(CrmReceivablePlanDO::getRemindTime, reqVO.getRemindTime())
+                .eqIfPresent(CrmReceivablePlanDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(CrmReceivablePlanDO::getContractId, reqVO.getContractId())
+                .eqIfPresent(CrmReceivablePlanDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(CrmReceivablePlanDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmReceivablePlanDO::getId));
+    }
+
+    default List<CrmReceivablePlanDO> selectList(CrmReceivablePlanExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CrmReceivablePlanDO>()
+                .eqIfPresent(CrmReceivablePlanDO::getPeriod, reqVO.getPeriod())
+                .eqIfPresent(CrmReceivablePlanDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(CrmReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
+                .betweenIfPresent(CrmReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(CrmReceivablePlanDO::getRemindDays, reqVO.getRemindDays())
+                .betweenIfPresent(CrmReceivablePlanDO::getRemindTime, reqVO.getRemindTime())
+                .eqIfPresent(CrmReceivablePlanDO::getCustomerId, reqVO.getCustomerId())
+                .eqIfPresent(CrmReceivablePlanDO::getContractId, reqVO.getContractId())
+                .eqIfPresent(CrmReceivablePlanDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .eqIfPresent(CrmReceivablePlanDO::getRemark, reqVO.getRemark())
+                .betweenIfPresent(CrmReceivablePlanDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(CrmReceivablePlanDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
deleted file mode 100644
index 0ef7165f4..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivableMapper.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
-
-import java.util.*;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
-import org.apache.ibatis.annotations.Mapper;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-
-/**
- * 回款管理 Mapper
- *
- * @author 赤焰
- */
-@Mapper
-public interface ReceivableMapper extends BaseMapperX<ReceivableDO> {
-
-    default PageResult<ReceivableDO> selectPage(ReceivablePageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<ReceivableDO>()
-                .eqIfPresent(ReceivableDO::getNo, reqVO.getNo())
-                .eqIfPresent(ReceivableDO::getPlanId, reqVO.getPlanId())
-                .eqIfPresent(ReceivableDO::getCustomerId, reqVO.getCustomerId())
-                .eqIfPresent(ReceivableDO::getContractId, reqVO.getContractId())
-                .eqIfPresent(ReceivableDO::getCheckStatus, reqVO.getCheckStatus())
-                .betweenIfPresent(ReceivableDO::getReturnTime, reqVO.getReturnTime())
-                .eqIfPresent(ReceivableDO::getReturnType, reqVO.getReturnType())
-                .eqIfPresent(ReceivableDO::getPrice, reqVO.getPrice())
-                .eqIfPresent(ReceivableDO::getOwnerUserId, reqVO.getOwnerUserId())
-                .eqIfPresent(ReceivableDO::getStatus, reqVO.getStatus())
-                .betweenIfPresent(ReceivableDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(ReceivableDO::getId));
-    }
-
-    default List<ReceivableDO> selectList(ReceivableExportReqVO reqVO) {
-        return selectList(new LambdaQueryWrapperX<ReceivableDO>()
-                .eqIfPresent(ReceivableDO::getNo, reqVO.getNo())
-                .eqIfPresent(ReceivableDO::getPlanId, reqVO.getPlanId())
-                .eqIfPresent(ReceivableDO::getCustomerId, reqVO.getCustomerId())
-                .eqIfPresent(ReceivableDO::getContractId, reqVO.getContractId())
-                .eqIfPresent(ReceivableDO::getCheckStatus, reqVO.getCheckStatus())
-                .betweenIfPresent(ReceivableDO::getReturnTime, reqVO.getReturnTime())
-                .eqIfPresent(ReceivableDO::getReturnType, reqVO.getReturnType())
-                .eqIfPresent(ReceivableDO::getPrice, reqVO.getPrice())
-                .eqIfPresent(ReceivableDO::getOwnerUserId, reqVO.getOwnerUserId())
-                .eqIfPresent(ReceivableDO::getBatchId, reqVO.getBatchId())
-                .eqIfPresent(ReceivableDO::getStatus, reqVO.getStatus())
-                .eqIfPresent(ReceivableDO::getRemark, reqVO.getRemark())
-                .betweenIfPresent(ReceivableDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(ReceivableDO::getId));
-    }
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
deleted file mode 100644
index 516f20c0c..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/ReceivablePlanMapper.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
-
-import java.util.*;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
-import org.apache.ibatis.annotations.Mapper;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-
-/**
- * 回款计划 Mapper
- *
- * @author 芋道源码
- */
-@Mapper
-public interface ReceivablePlanMapper extends BaseMapperX<ReceivablePlanDO> {
-
-    default PageResult<ReceivablePlanDO> selectPage(ReceivablePlanPageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<ReceivablePlanDO>()
-                .eqIfPresent(ReceivablePlanDO::getStatus, reqVO.getStatus())
-                .eqIfPresent(ReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
-                .betweenIfPresent(ReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
-                .betweenIfPresent(ReceivablePlanDO::getRemindTime, reqVO.getRemindTime())
-                .eqIfPresent(ReceivablePlanDO::getCustomerId, reqVO.getCustomerId())
-                .eqIfPresent(ReceivablePlanDO::getContractId, reqVO.getContractId())
-                .eqIfPresent(ReceivablePlanDO::getOwnerUserId, reqVO.getOwnerUserId())
-                .betweenIfPresent(ReceivablePlanDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(ReceivablePlanDO::getId));
-    }
-
-    default List<ReceivablePlanDO> selectList(ReceivablePlanExportReqVO reqVO) {
-        return selectList(new LambdaQueryWrapperX<ReceivablePlanDO>()
-                .eqIfPresent(ReceivablePlanDO::getPeriod, reqVO.getPeriod())
-                .eqIfPresent(ReceivablePlanDO::getStatus, reqVO.getStatus())
-                .eqIfPresent(ReceivablePlanDO::getCheckStatus, reqVO.getCheckStatus())
-                .betweenIfPresent(ReceivablePlanDO::getReturnTime, reqVO.getReturnTime())
-                .eqIfPresent(ReceivablePlanDO::getRemindDays, reqVO.getRemindDays())
-                .betweenIfPresent(ReceivablePlanDO::getRemindTime, reqVO.getRemindTime())
-                .eqIfPresent(ReceivablePlanDO::getCustomerId, reqVO.getCustomerId())
-                .eqIfPresent(ReceivablePlanDO::getContractId, reqVO.getContractId())
-                .eqIfPresent(ReceivablePlanDO::getOwnerUserId, reqVO.getOwnerUserId())
-                .eqIfPresent(ReceivablePlanDO::getRemark, reqVO.getRemark())
-                .betweenIfPresent(ReceivablePlanDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(ReceivablePlanDO::getId));
-    }
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java
similarity index 64%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanService.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java
index 163ebc26a..b8c472fa3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.service.receivable;
 import java.util.*;
 import javax.validation.*;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 
 /**
@@ -11,7 +11,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
  *
  * @author 芋道源码
  */
-public interface ReceivablePlanService {
+public interface CrmReceivablePlanService {
 
     /**
      * 创建回款计划
@@ -19,14 +19,14 @@ public interface ReceivablePlanService {
      * @param createReqVO 创建信息
      * @return 编号
      */
-    Long createReceivablePlan(@Valid ReceivablePlanCreateReqVO createReqVO);
+    Long createReceivablePlan(@Valid CrmReceivablePlanCreateReqVO createReqVO);
 
     /**
      * 更新回款计划
      *
      * @param updateReqVO 更新信息
      */
-    void updateReceivablePlan(@Valid ReceivablePlanUpdateReqVO updateReqVO);
+    void updateReceivablePlan(@Valid CrmReceivablePlanUpdateReqVO updateReqVO);
 
     /**
      * 删除回款计划
@@ -41,7 +41,7 @@ public interface ReceivablePlanService {
      * @param id 编号
      * @return 回款计划
      */
-    ReceivablePlanDO getReceivablePlan(Long id);
+    CrmReceivablePlanDO getReceivablePlan(Long id);
 
     /**
      * 获得回款计划列表
@@ -49,7 +49,7 @@ public interface ReceivablePlanService {
      * @param ids 编号
      * @return 回款计划列表
      */
-    List<ReceivablePlanDO> getReceivablePlanList(Collection<Long> ids);
+    List<CrmReceivablePlanDO> getReceivablePlanList(Collection<Long> ids);
 
     /**
      * 获得回款计划分页
@@ -57,7 +57,7 @@ public interface ReceivablePlanService {
      * @param pageReqVO 分页查询
      * @return 回款计划分页
      */
-    PageResult<ReceivablePlanDO> getReceivablePlanPage(ReceivablePlanPageReqVO pageReqVO);
+    PageResult<CrmReceivablePlanDO> getReceivablePlanPage(CrmReceivablePlanPageReqVO pageReqVO);
 
     /**
      * 获得回款计划列表, 用于 Excel 导出
@@ -65,6 +65,6 @@ public interface ReceivablePlanService {
      * @param exportReqVO 查询条件
      * @return 回款计划列表
      */
-    List<ReceivablePlanDO> getReceivablePlanList(ReceivablePlanExportReqVO exportReqVO);
+    List<CrmReceivablePlanDO> getReceivablePlanList(CrmReceivablePlanExportReqVO exportReqVO);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
similarity index 63%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
index f8493a1ec..b3497b686 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
@@ -5,16 +5,15 @@ 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.module.crm.controller.admin.receivable.vo.ReceivablePlanCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanUpdateReqVO;
-import cn.iocoder.yudao.module.crm.convert.receivable.ReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePlanCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePlanExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePlanPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePlanUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivablePlanConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
-import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper;
 import cn.iocoder.yudao.module.crm.enums.AuditStatusEnum;
 import cn.iocoder.yudao.module.crm.service.contract.ContractService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
@@ -35,19 +34,19 @@ import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
  */
 @Service
 @Validated
-public class ReceivablePlanServiceImpl implements ReceivablePlanService {
+public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
 
     @Resource
-    private ReceivablePlanMapper receivablePlanMapper;
+    private CrmReceivablePlanMapper crmReceivablePlanMapper;
     @Resource
     private ContractService contractService;
     @Resource
     private CrmCustomerService crmCustomerService;
 
     @Override
-    public Long createReceivablePlan(ReceivablePlanCreateReqVO createReqVO) {
+    public Long createReceivablePlan(CrmReceivablePlanCreateReqVO createReqVO) {
         // 插入
-        ReceivablePlanDO receivablePlan = ReceivablePlanConvert.INSTANCE.convert(createReqVO);
+        CrmReceivablePlanDO receivablePlan = CrmReceivablePlanConvert.INSTANCE.convert(createReqVO);
         if (ObjectUtil.isNull(receivablePlan.getStatus())){
             receivablePlan.setStatus(CommonStatusEnum.ENABLE.getStatus());
         }
@@ -57,12 +56,12 @@ public class ReceivablePlanServiceImpl implements ReceivablePlanService {
 
         checkReceivablePlan(receivablePlan);
 
-        receivablePlanMapper.insert(receivablePlan);
+        crmReceivablePlanMapper.insert(receivablePlan);
         // 返回
         return receivablePlan.getId();
     }
 
-    private void checkReceivablePlan(ReceivablePlanDO receivablePlan) {
+    private void checkReceivablePlan(CrmReceivablePlanDO receivablePlan) {
 
         if(ObjectUtil.isNull(receivablePlan.getContractId())){
             throw exception(CONTRACT_NOT_EXISTS);
@@ -81,13 +80,13 @@ public class ReceivablePlanServiceImpl implements ReceivablePlanService {
     }
 
     @Override
-    public void updateReceivablePlan(ReceivablePlanUpdateReqVO updateReqVO) {
+    public void updateReceivablePlan(CrmReceivablePlanUpdateReqVO updateReqVO) {
         // 校验存在
         validateReceivablePlanExists(updateReqVO.getId());
 
         // 更新
-        ReceivablePlanDO updateObj = ReceivablePlanConvert.INSTANCE.convert(updateReqVO);
-        receivablePlanMapper.updateById(updateObj);
+        CrmReceivablePlanDO updateObj = CrmReceivablePlanConvert.INSTANCE.convert(updateReqVO);
+        crmReceivablePlanMapper.updateById(updateObj);
     }
 
     @Override
@@ -95,36 +94,36 @@ public class ReceivablePlanServiceImpl implements ReceivablePlanService {
         // 校验存在
         validateReceivablePlanExists(id);
         // 删除
-        receivablePlanMapper.deleteById(id);
+        crmReceivablePlanMapper.deleteById(id);
     }
 
     private void validateReceivablePlanExists(Long id) {
-        if (receivablePlanMapper.selectById(id) == null) {
+        if (crmReceivablePlanMapper.selectById(id) == null) {
             throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
         }
     }
 
     @Override
-    public ReceivablePlanDO getReceivablePlan(Long id) {
-        return receivablePlanMapper.selectById(id);
+    public CrmReceivablePlanDO getReceivablePlan(Long id) {
+        return crmReceivablePlanMapper.selectById(id);
     }
 
     @Override
-    public List<ReceivablePlanDO> getReceivablePlanList(Collection<Long> ids) {
+    public List<CrmReceivablePlanDO> getReceivablePlanList(Collection<Long> ids) {
         if (CollUtil.isEmpty(ids)) {
             return ListUtil.empty();
         }
-        return receivablePlanMapper.selectBatchIds(ids);
+        return crmReceivablePlanMapper.selectBatchIds(ids);
     }
 
     @Override
-    public PageResult<ReceivablePlanDO> getReceivablePlanPage(ReceivablePlanPageReqVO pageReqVO) {
-        return receivablePlanMapper.selectPage(pageReqVO);
+    public PageResult<CrmReceivablePlanDO> getReceivablePlanPage(CrmReceivablePlanPageReqVO pageReqVO) {
+        return crmReceivablePlanMapper.selectPage(pageReqVO);
     }
 
     @Override
-    public List<ReceivablePlanDO> getReceivablePlanList(ReceivablePlanExportReqVO exportReqVO) {
-        return receivablePlanMapper.selectList(exportReqVO);
+    public List<CrmReceivablePlanDO> getReceivablePlanList(CrmReceivablePlanExportReqVO exportReqVO) {
+        return crmReceivablePlanMapper.selectList(exportReqVO);
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
similarity index 67%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableService.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
index a673ec99e..8875faaa9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
@@ -4,7 +4,7 @@ import java.util.*;
 import javax.validation.*;
 
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 
 /**
@@ -12,7 +12,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
  *
  * @author 赤焰
  */
-public interface ReceivableService {
+public interface CrmReceivableService {
 
     /**
      * 创建回款管理
@@ -20,14 +20,14 @@ public interface ReceivableService {
      * @param createReqVO 创建信息
      * @return 编号
      */
-    Long createReceivable(@Valid ReceivableCreateReqVO createReqVO);
+    Long createReceivable(@Valid CrmReceivableCreateReqVO createReqVO);
 
     /**
      * 更新回款管理
      *
      * @param updateReqVO 更新信息
      */
-    void updateReceivable(@Valid ReceivableUpdateReqVO updateReqVO);
+    void updateReceivable(@Valid CrmReceivableUpdateReqVO updateReqVO);
 
     /**
      * 删除回款管理
@@ -42,7 +42,7 @@ public interface ReceivableService {
      * @param id 编号
      * @return 回款管理
      */
-    ReceivableDO getReceivable(Long id);
+    CrmReceivableDO getReceivable(Long id);
 
     /**
      * 获得回款管理列表
@@ -50,7 +50,7 @@ public interface ReceivableService {
      * @param ids 编号
      * @return 回款管理列表
      */
-    List<ReceivableDO> getReceivableList(Collection<Long> ids);
+    List<CrmReceivableDO> getReceivableList(Collection<Long> ids);
 
     /**
      * 获得回款管理分页
@@ -58,7 +58,7 @@ public interface ReceivableService {
      * @param pageReqVO 分页查询
      * @return 回款管理分页
      */
-    PageResult<ReceivableDO> getReceivablePage(ReceivablePageReqVO pageReqVO);
+    PageResult<CrmReceivableDO> getReceivablePage(CrmReceivablePageReqVO pageReqVO);
 
     /**
      * 获得回款管理列表, 用于 Excel 导出
@@ -66,6 +66,6 @@ public interface ReceivableService {
      * @param exportReqVO 查询条件
      * @return 回款管理列表
      */
-    List<ReceivableDO> getReceivableList(ReceivableExportReqVO exportReqVO);
+    List<CrmReceivableDO> getReceivableList(CrmReceivableExportReqVO exportReqVO);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
similarity index 62%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
index d3c7a16ef..ff751bc51 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -3,19 +3,18 @@ package cn.iocoder.yudao.module.crm.service.receivable;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableUpdateReqVO;
-import cn.iocoder.yudao.module.crm.convert.receivable.ReceivableConvert;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivableCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivableExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivableUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivableConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
-import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper;
 import cn.iocoder.yudao.module.crm.enums.AuditStatusEnum;
 import cn.iocoder.yudao.module.crm.service.contract.ContractService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
@@ -36,21 +35,21 @@ import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
  */
 @Service
 @Validated
-public class ReceivableServiceImpl implements ReceivableService {
+public class CrmReceivableServiceImpl implements CrmReceivableService {
 
     @Resource
-    private ReceivableMapper receivableMapper;
+    private CrmReceivableMapper crmReceivableMapper;
     @Resource
     private ContractService contractService;
     @Resource
     private CrmCustomerService crmCustomerService;
     @Resource
-    private ReceivablePlanService receivablePlanService;
+    private CrmReceivablePlanService crmReceivablePlanService;
 
     @Override
-    public Long createReceivable(ReceivableCreateReqVO createReqVO) {
+    public Long createReceivable(CrmReceivableCreateReqVO createReqVO) {
         // 插入
-        ReceivableDO receivable = ReceivableConvert.INSTANCE.convert(createReqVO);
+        CrmReceivableDO receivable = CrmReceivableConvert.INSTANCE.convert(createReqVO);
         if (ObjectUtil.isNull(receivable.getStatus())){
             receivable.setStatus(CommonStatusEnum.ENABLE.getStatus());
         }
@@ -61,12 +60,12 @@ public class ReceivableServiceImpl implements ReceivableService {
         //校验
         checkReceivable(receivable);
 
-        receivableMapper.insert(receivable);
+        crmReceivableMapper.insert(receivable);
         // 返回
         return receivable.getId();
     }
 
-    private void checkReceivable(ReceivableDO receivable) {
+    private void checkReceivable(CrmReceivableDO receivable) {
 
         if(ObjectUtil.isNull(receivable.getContractId())){
             throw exception(CONTRACT_NOT_EXISTS);
@@ -82,7 +81,7 @@ public class ReceivableServiceImpl implements ReceivableService {
             throw exception(CUSTOMER_NOT_EXISTS);
         }
 
-        ReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(receivable.getPlanId());
+        CrmReceivablePlanDO receivablePlan = crmReceivablePlanService.getReceivablePlan(receivable.getPlanId());
         if(ObjectUtil.isNull(receivablePlan)){
             throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
         }
@@ -90,13 +89,13 @@ public class ReceivableServiceImpl implements ReceivableService {
     }
 
     @Override
-    public void updateReceivable(ReceivableUpdateReqVO updateReqVO) {
+    public void updateReceivable(CrmReceivableUpdateReqVO updateReqVO) {
         // 校验存在
         validateReceivableExists(updateReqVO.getId());
 
         // 更新
-        ReceivableDO updateObj = ReceivableConvert.INSTANCE.convert(updateReqVO);
-        receivableMapper.updateById(updateObj);
+        CrmReceivableDO updateObj = CrmReceivableConvert.INSTANCE.convert(updateReqVO);
+        crmReceivableMapper.updateById(updateObj);
     }
 
     @Override
@@ -104,36 +103,36 @@ public class ReceivableServiceImpl implements ReceivableService {
         // 校验存在
         validateReceivableExists(id);
         // 删除
-        receivableMapper.deleteById(id);
+        crmReceivableMapper.deleteById(id);
     }
 
     private void validateReceivableExists(Long id) {
-        if (receivableMapper.selectById(id) == null) {
+        if (crmReceivableMapper.selectById(id) == null) {
             throw exception(RECEIVABLE_NOT_EXISTS);
         }
     }
 
     @Override
-    public ReceivableDO getReceivable(Long id) {
-        return receivableMapper.selectById(id);
+    public CrmReceivableDO getReceivable(Long id) {
+        return crmReceivableMapper.selectById(id);
     }
 
     @Override
-    public List<ReceivableDO> getReceivableList(Collection<Long> ids) {
+    public List<CrmReceivableDO> getReceivableList(Collection<Long> ids) {
         if (CollUtil.isEmpty(ids)) {
             return ListUtil.empty();
         }
-        return receivableMapper.selectBatchIds(ids);
+        return crmReceivableMapper.selectBatchIds(ids);
     }
 
     @Override
-    public PageResult<ReceivableDO> getReceivablePage(ReceivablePageReqVO pageReqVO) {
-        return receivableMapper.selectPage(pageReqVO);
+    public PageResult<CrmReceivableDO> getReceivablePage(CrmReceivablePageReqVO pageReqVO) {
+        return crmReceivableMapper.selectPage(pageReqVO);
     }
 
     @Override
-    public List<ReceivableDO> getReceivableList(ReceivableExportReqVO exportReqVO) {
-        return receivableMapper.selectList(exportReqVO);
+    public List<CrmReceivableDO> getReceivableList(CrmReceivableExportReqVO exportReqVO) {
+        return crmReceivableMapper.selectList(exportReqVO);
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivableMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivableMapper.xml
similarity index 95%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivableMapper.xml
rename to yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivableMapper.xml
index b87beb08f..b2ab2042e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivableMapper.xml
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivableMapper.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper">
 
     <!--
         一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivablePlanMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivablePlanMapper.xml
similarity index 95%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivablePlanMapper.xml
rename to yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivablePlanMapper.xml
index f0e4c2e84..3b18148de 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/ReceivablePlanMapper.xml
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivablePlanMapper.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper">
 
     <!--
         一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivablePlanServiceImplTest.java
similarity index 58%
rename from yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
rename to yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivablePlanServiceImplTest.java
index 952514ecb..09993f899 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivablePlanServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivablePlanServiceImplTest.java
@@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.crm.service.receivable;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePlanUpdateReqVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivablePlanDO;
-import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivablePlanMapper;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePlanCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePlanExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePlanPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePlanUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.context.annotation.Import;
@@ -26,54 +26,54 @@ import static org.junit.jupiter.api.Assertions.*;
 
 // TODO 芋艿:后续,需要补充测试用例
 /**
- * {@link ReceivablePlanServiceImpl} 的单元测试类
+ * {@link CrmReceivablePlanServiceImpl} 的单元测试类
  *
  * @author 芋道源码
  */
-@Import(ReceivablePlanServiceImpl.class)
-public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
+@Import(CrmReceivablePlanServiceImpl.class)
+public class CrmCrmReceivablePlanServiceImplTest extends BaseDbUnitTest {
 
     @Resource
-    private ReceivablePlanServiceImpl receivablePlanService;
+    private CrmReceivablePlanServiceImpl receivablePlanService;
 
     @Resource
-    private ReceivablePlanMapper receivablePlanMapper;
+    private CrmReceivablePlanMapper crmReceivablePlanMapper;
 
     @Test
     public void testCreateReceivablePlan_success() {
         // 准备参数
-        ReceivablePlanCreateReqVO reqVO = randomPojo(ReceivablePlanCreateReqVO.class);
+        CrmReceivablePlanCreateReqVO reqVO = randomPojo(CrmReceivablePlanCreateReqVO.class);
 
         // 调用
         Long receivablePlanId = receivablePlanService.createReceivablePlan(reqVO);
         // 断言
         assertNotNull(receivablePlanId);
         // 校验记录的属性是否正确
-        ReceivablePlanDO receivablePlan = receivablePlanMapper.selectById(receivablePlanId);
+        CrmReceivablePlanDO receivablePlan = crmReceivablePlanMapper.selectById(receivablePlanId);
         assertPojoEquals(reqVO, receivablePlan);
     }
 
     @Test
     public void testUpdateReceivablePlan_success() {
         // mock 数据
-        ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class);
-        receivablePlanMapper.insert(dbReceivablePlan);// @Sql: 先插入出一条存在的数据
+        CrmReceivablePlanDO dbReceivablePlan = randomPojo(CrmReceivablePlanDO.class);
+        crmReceivablePlanMapper.insert(dbReceivablePlan);// @Sql: 先插入出一条存在的数据
         // 准备参数
-        ReceivablePlanUpdateReqVO reqVO = randomPojo(ReceivablePlanUpdateReqVO.class, o -> {
+        CrmReceivablePlanUpdateReqVO reqVO = randomPojo(CrmReceivablePlanUpdateReqVO.class, o -> {
             o.setId(dbReceivablePlan.getId()); // 设置更新的 ID
         });
 
         // 调用
         receivablePlanService.updateReceivablePlan(reqVO);
         // 校验是否更新正确
-        ReceivablePlanDO receivablePlan = receivablePlanMapper.selectById(reqVO.getId()); // 获取最新的
+        CrmReceivablePlanDO receivablePlan = crmReceivablePlanMapper.selectById(reqVO.getId()); // 获取最新的
         assertPojoEquals(reqVO, receivablePlan);
     }
 
     @Test
     public void testUpdateReceivablePlan_notExists() {
         // 准备参数
-        ReceivablePlanUpdateReqVO reqVO = randomPojo(ReceivablePlanUpdateReqVO.class);
+        CrmReceivablePlanUpdateReqVO reqVO = randomPojo(CrmReceivablePlanUpdateReqVO.class);
 
         // 调用, 并断言异常
         assertServiceException(() -> receivablePlanService.updateReceivablePlan(reqVO), RECEIVABLE_PLAN_NOT_EXISTS);
@@ -82,15 +82,15 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
     @Test
     public void testDeleteReceivablePlan_success() {
         // mock 数据
-        ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class);
-        receivablePlanMapper.insert(dbReceivablePlan);// @Sql: 先插入出一条存在的数据
+        CrmReceivablePlanDO dbReceivablePlan = randomPojo(CrmReceivablePlanDO.class);
+        crmReceivablePlanMapper.insert(dbReceivablePlan);// @Sql: 先插入出一条存在的数据
         // 准备参数
         Long id = dbReceivablePlan.getId();
 
         // 调用
         receivablePlanService.deleteReceivablePlan(id);
        // 校验数据不存在了
-       assertNull(receivablePlanMapper.selectById(id));
+       assertNull(crmReceivablePlanMapper.selectById(id));
     }
 
     @Test
@@ -106,7 +106,7 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
     @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
     public void testGetReceivablePlanPage() {
        // mock 数据
-       ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class, o -> { // 等会查询到
+       CrmReceivablePlanDO dbReceivablePlan = randomPojo(CrmReceivablePlanDO.class, o -> { // 等会查询到
            o.setPeriod(null);
            o.setStatus(null);
            o.setCheckStatus(null);
@@ -119,31 +119,31 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
            o.setRemark(null);
            o.setCreateTime(null);
        });
-       receivablePlanMapper.insert(dbReceivablePlan);
+       crmReceivablePlanMapper.insert(dbReceivablePlan);
        // 测试 Period 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setPeriod(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setPeriod(null)));
        // 测试 status 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
        // 测试 checkStatus 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCheckStatus(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCheckStatus(null)));
        // 测试 returnTime 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setReturnTime(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setReturnTime(null)));
        // 测试 remindDays 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindDays(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindDays(null)));
        // 测试 remindTime 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindTime(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindTime(null)));
        // 测试 customerId 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCustomerId(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCustomerId(null)));
        // 测试 contractId 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setContractId(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setContractId(null)));
        // 测试 ownerUserId 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setOwnerUserId(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setOwnerUserId(null)));
        // 测试 remark 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemark(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemark(null)));
        // 测试 createTime 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
        // 准备参数
-       ReceivablePlanPageReqVO reqVO = new ReceivablePlanPageReqVO();
+       CrmReceivablePlanPageReqVO reqVO = new CrmReceivablePlanPageReqVO();
        reqVO.setStatus(null);
        reqVO.setCheckStatus(null);
        reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
@@ -154,7 +154,7 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
        reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
 
        // 调用
-       PageResult<ReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(reqVO);
+       PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(reqVO);
        // 断言
        assertEquals(1, pageResult.getTotal());
        assertEquals(1, pageResult.getList().size());
@@ -165,7 +165,7 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
     @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
     public void testGetReceivablePlanList() {
        // mock 数据
-       ReceivablePlanDO dbReceivablePlan = randomPojo(ReceivablePlanDO.class, o -> { // 等会查询到
+       CrmReceivablePlanDO dbReceivablePlan = randomPojo(CrmReceivablePlanDO.class, o -> { // 等会查询到
            o.setPeriod(null);
            o.setStatus(null);
            o.setCheckStatus(null);
@@ -178,31 +178,31 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
            o.setRemark(null);
            o.setCreateTime(null);
        });
-       receivablePlanMapper.insert(dbReceivablePlan);
+       crmReceivablePlanMapper.insert(dbReceivablePlan);
        // 测试 Period 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setPeriod(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setPeriod(null)));
        // 测试 status 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
        // 测试 checkStatus 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCheckStatus(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCheckStatus(null)));
        // 测试 returnTime 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setReturnTime(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setReturnTime(null)));
        // 测试 remindDays 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindDays(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindDays(null)));
        // 测试 remindTime 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindTime(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindTime(null)));
        // 测试 customerId 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCustomerId(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCustomerId(null)));
        // 测试 contractId 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setContractId(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setContractId(null)));
        // 测试 ownerUserId 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setOwnerUserId(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setOwnerUserId(null)));
        // 测试 remark 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemark(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemark(null)));
        // 测试 createTime 不匹配
-       receivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
        // 准备参数
-       ReceivablePlanExportReqVO reqVO = new ReceivablePlanExportReqVO();
+       CrmReceivablePlanExportReqVO reqVO = new CrmReceivablePlanExportReqVO();
        reqVO.setPeriod(null);
        reqVO.setStatus(null);
        reqVO.setCheckStatus(null);
@@ -216,7 +216,7 @@ public class ReceivablePlanServiceImplTest extends BaseDbUnitTest {
        reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
 
        // 调用
-       List<ReceivablePlanDO> list = receivablePlanService.getReceivablePlanList(reqVO);
+       List<CrmReceivablePlanDO> list = receivablePlanService.getReceivablePlanList(reqVO);
        // 断言
        assertEquals(1, list.size());
        assertPojoEquals(dbReceivablePlan, list.get(0));
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivableServiceImplTest.java
similarity index 57%
rename from yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
rename to yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivableServiceImplTest.java
index 906fdb436..6d65231b1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/ReceivableServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivableServiceImplTest.java
@@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.crm.service.receivable;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableCreateReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivablePageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.ReceivableUpdateReqVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.ReceivableDO;
-import cn.iocoder.yudao.module.crm.dal.mysql.receivable.ReceivableMapper;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivableCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivableExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivablePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.CrmReceivableUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.context.annotation.Import;
@@ -26,54 +26,54 @@ import static org.junit.jupiter.api.Assertions.*;
 
 // TODO 芋艿:等实现完,在校验下;
 /**
- * {@link ReceivableServiceImpl} 的单元测试类
+ * {@link CrmReceivableServiceImpl} 的单元测试类
  *
  * @author 赤焰
  */
-@Import(ReceivableServiceImpl.class)
-public class ReceivableServiceImplTest extends BaseDbUnitTest {
+@Import(CrmReceivableServiceImpl.class)
+public class CrmCrmReceivableServiceImplTest extends BaseDbUnitTest {
 
     @Resource
-    private ReceivableServiceImpl receivableService;
+    private CrmReceivableServiceImpl receivableService;
 
     @Resource
-    private ReceivableMapper receivableMapper;
+    private CrmReceivableMapper crmReceivableMapper;
 
     @Test
     public void testCreateReceivable_success() {
         // 准备参数
-        ReceivableCreateReqVO reqVO = randomPojo(ReceivableCreateReqVO.class);
+        CrmReceivableCreateReqVO reqVO = randomPojo(CrmReceivableCreateReqVO.class);
 
         // 调用
         Long receivableId = receivableService.createReceivable(reqVO);
         // 断言
         assertNotNull(receivableId);
         // 校验记录的属性是否正确
-        ReceivableDO receivable = receivableMapper.selectById(receivableId);
+        CrmReceivableDO receivable = crmReceivableMapper.selectById(receivableId);
         assertPojoEquals(reqVO, receivable);
     }
 
     @Test
     public void testUpdateReceivable_success() {
         // mock 数据
-        ReceivableDO dbReceivable = randomPojo(ReceivableDO.class);
-        receivableMapper.insert(dbReceivable);// @Sql: 先插入出一条存在的数据
+        CrmReceivableDO dbReceivable = randomPojo(CrmReceivableDO.class);
+        crmReceivableMapper.insert(dbReceivable);// @Sql: 先插入出一条存在的数据
         // 准备参数
-        ReceivableUpdateReqVO reqVO = randomPojo(ReceivableUpdateReqVO.class, o -> {
+        CrmReceivableUpdateReqVO reqVO = randomPojo(CrmReceivableUpdateReqVO.class, o -> {
             o.setId(dbReceivable.getId()); // 设置更新的 ID
         });
 
         // 调用
         receivableService.updateReceivable(reqVO);
         // 校验是否更新正确
-        ReceivableDO receivable = receivableMapper.selectById(reqVO.getId()); // 获取最新的
+        CrmReceivableDO receivable = crmReceivableMapper.selectById(reqVO.getId()); // 获取最新的
         assertPojoEquals(reqVO, receivable);
     }
 
     @Test
     public void testUpdateReceivable_notExists() {
         // 准备参数
-        ReceivableUpdateReqVO reqVO = randomPojo(ReceivableUpdateReqVO.class);
+        CrmReceivableUpdateReqVO reqVO = randomPojo(CrmReceivableUpdateReqVO.class);
 
         // 调用, 并断言异常
         assertServiceException(() -> receivableService.updateReceivable(reqVO), RECEIVABLE_NOT_EXISTS);
@@ -82,15 +82,15 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
     @Test
     public void testDeleteReceivable_success() {
         // mock 数据
-        ReceivableDO dbReceivable = randomPojo(ReceivableDO.class);
-        receivableMapper.insert(dbReceivable);// @Sql: 先插入出一条存在的数据
+        CrmReceivableDO dbReceivable = randomPojo(CrmReceivableDO.class);
+        crmReceivableMapper.insert(dbReceivable);// @Sql: 先插入出一条存在的数据
         // 准备参数
         Long id = dbReceivable.getId();
 
         // 调用
         receivableService.deleteReceivable(id);
        // 校验数据不存在了
-       assertNull(receivableMapper.selectById(id));
+       assertNull(crmReceivableMapper.selectById(id));
     }
 
     @Test
@@ -106,7 +106,7 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
     @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
     public void testGetReceivablePage() {
        // mock 数据
-       ReceivableDO dbReceivable = randomPojo(ReceivableDO.class, o -> { // 等会查询到
+       CrmReceivableDO dbReceivable = randomPojo(CrmReceivableDO.class, o -> { // 等会查询到
            o.setNo(null);
            o.setPlanId(null);
            o.setCustomerId(null);
@@ -125,43 +125,43 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
            o.setRemark(null);
            o.setCreateTime(null);
        });
-       receivableMapper.insert(dbReceivable);
+       crmReceivableMapper.insert(dbReceivable);
        // 测试 no 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setNo(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setNo(null)));
        // 测试 planId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPlanId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPlanId(null)));
        // 测试 customerId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCustomerId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCustomerId(null)));
        // 测试 contractId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setContractId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setContractId(null)));
        // 测试 checkStatus 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCheckStatus(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCheckStatus(null)));
        // 测试 processInstanceId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setProcessInstanceId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setProcessInstanceId(null)));
        // 测试 returnTime 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnTime(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnTime(null)));
        // 测试 returnType 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnType(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnType(null)));
        // 测试 price 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPrice(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPrice(null)));
        // 测试 ownerUserId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setOwnerUserId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setOwnerUserId(null)));
        // 测试 batchId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setBatchId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setBatchId(null)));
        // 测试 sort 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setSort(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setSort(null)));
        // 测试 dataScope 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScope(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScope(null)));
        // 测试 dataScopeDeptIds 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScopeDeptIds(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScopeDeptIds(null)));
        // 测试 status 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setStatus(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setStatus(null)));
        // 测试 remark 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setRemark(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setRemark(null)));
        // 测试 createTime 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCreateTime(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCreateTime(null)));
        // 准备参数
-       ReceivablePageReqVO reqVO = new ReceivablePageReqVO();
+       CrmReceivablePageReqVO reqVO = new CrmReceivablePageReqVO();
        reqVO.setNo(null);
        reqVO.setPlanId(null);
        reqVO.setCustomerId(null);
@@ -175,7 +175,7 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
        reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
 
        // 调用
-       PageResult<ReceivableDO> pageResult = receivableService.getReceivablePage(reqVO);
+       PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePage(reqVO);
        // 断言
        assertEquals(1, pageResult.getTotal());
        assertEquals(1, pageResult.getList().size());
@@ -186,7 +186,7 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
     @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
     public void testGetReceivableList() {
        // mock 数据
-       ReceivableDO dbReceivable = randomPojo(ReceivableDO.class, o -> { // 等会查询到
+       CrmReceivableDO dbReceivable = randomPojo(CrmReceivableDO.class, o -> { // 等会查询到
            o.setNo(null);
            o.setPlanId(null);
            o.setCustomerId(null);
@@ -205,43 +205,43 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
            o.setRemark(null);
            o.setCreateTime(null);
        });
-       receivableMapper.insert(dbReceivable);
+       crmReceivableMapper.insert(dbReceivable);
        // 测试 no 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setNo(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setNo(null)));
        // 测试 planId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPlanId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPlanId(null)));
        // 测试 customerId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCustomerId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCustomerId(null)));
        // 测试 contractId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setContractId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setContractId(null)));
        // 测试 checkStatus 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCheckStatus(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCheckStatus(null)));
        // 测试 processInstanceId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setProcessInstanceId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setProcessInstanceId(null)));
        // 测试 returnTime 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnTime(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnTime(null)));
        // 测试 returnType 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnType(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnType(null)));
        // 测试 price 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPrice(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPrice(null)));
        // 测试 ownerUserId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setOwnerUserId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setOwnerUserId(null)));
        // 测试 batchId 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setBatchId(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setBatchId(null)));
        // 测试 sort 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setSort(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setSort(null)));
        // 测试 dataScope 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScope(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScope(null)));
        // 测试 dataScopeDeptIds 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScopeDeptIds(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScopeDeptIds(null)));
        // 测试 status 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setStatus(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setStatus(null)));
        // 测试 remark 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setRemark(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setRemark(null)));
        // 测试 createTime 不匹配
-       receivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCreateTime(null)));
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCreateTime(null)));
        // 准备参数
-       ReceivableExportReqVO reqVO = new ReceivableExportReqVO();
+       CrmReceivableExportReqVO reqVO = new CrmReceivableExportReqVO();
        reqVO.setNo(null);
        reqVO.setPlanId(null);
        reqVO.setCustomerId(null);
@@ -257,7 +257,7 @@ public class ReceivableServiceImplTest extends BaseDbUnitTest {
        reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
 
        // 调用
-       List<ReceivableDO> list = receivableService.getReceivableList(reqVO);
+       List<CrmReceivableDO> list = receivableService.getReceivableList(reqVO);
        // 断言
        assertEquals(1, list.size());
        assertPojoEquals(dbReceivable, list.get(0));

From ab9dc8ad719f39493a9175da9ad8f49492f78419 Mon Sep 17 00:00:00 2001
From: wgs7909 <87356667@qq.com>
Date: Tue, 31 Oct 2023 06:39:57 +0800
Subject: [PATCH 048/101] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?=
 =?UTF-8?q?=E6=97=B6=EF=BC=8C=E7=BB=99=E9=83=A8=E9=97=A8=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E3=80=82=20=E5=AF=B9=E5=BA=94Issue,?=
 =?UTF-8?q?Cloud=E7=89=88=E6=9C=AC=E5=BA=93=E3=80=82=20https://gitee.com/z?=
 =?UTF-8?q?hijiantianya/yudao-cloud/issues/I8C4B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 yudao-ui-admin/src/views/system/user/index.vue | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/yudao-ui-admin/src/views/system/user/index.vue b/yudao-ui-admin/src/views/system/user/index.vue
index 1ad899616..87cb1355f 100644
--- a/yudao-ui-admin/src/views/system/user/index.vue
+++ b/yudao-ui-admin/src/views/system/user/index.vue
@@ -1,8 +1,5 @@
 <template>
   <div class="app-container">
-    <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
-    <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
-    <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
     <!-- 搜索工作栏 -->
     <el-row :gutter="20">
       <!--部门数据-->
@@ -479,6 +476,8 @@ export default {
       // 打开表单,并设置初始化
       this.open = true;
       this.title = "添加用户";
+      // 为部门增加默认值
+      this.form.deptId = this.queryParams.deptId;
       this.form.password = this.initPassword;
     },
     /** 修改按钮操作 */

From a0c142f8863b6e5297fc648a85b7ea2782bb673c Mon Sep 17 00:00:00 2001
From: wgs7909 <87356667@qq.com>
Date: Tue, 31 Oct 2023 06:42:12 +0800
Subject: [PATCH 049/101] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?=
 =?UTF-8?q?=E6=97=B6=EF=BC=8C=E7=BB=99=E9=83=A8=E9=97=A8=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E3=80=82=20=E5=AF=B9=E5=BA=94Issue,?=
 =?UTF-8?q?Cloud=E7=89=88=E6=9C=AC=E5=BA=93=E3=80=82=20https://gitee.com/z?=
 =?UTF-8?q?hijiantianya/yudao-cloud/issues/I8C4B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 yudao-ui-admin/src/views/system/user/index.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/yudao-ui-admin/src/views/system/user/index.vue b/yudao-ui-admin/src/views/system/user/index.vue
index 87cb1355f..c4f1b5297 100644
--- a/yudao-ui-admin/src/views/system/user/index.vue
+++ b/yudao-ui-admin/src/views/system/user/index.vue
@@ -1,5 +1,8 @@
 <template>
   <div class="app-container">
+    <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
+    <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
+    <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
     <!-- 搜索工作栏 -->
     <el-row :gutter="20">
       <!--部门数据-->

From 3dade7fd5497109aecb827106e5863368653d5aa Mon Sep 17 00:00:00 2001
From: wgs7909 <87356667@qq.com>
Date: Tue, 31 Oct 2023 07:07:11 +0800
Subject: [PATCH 050/101] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?=
 =?UTF-8?q?=E6=97=B6=EF=BC=8C=E7=BB=99=E9=83=A8=E9=97=A8=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC=E3=80=82=20=E5=AF=B9=E5=BA=94Issue,?=
 =?UTF-8?q?Cloud=E7=89=88=E6=9C=AC=E5=BA=93=E3=80=82=20https://gitee.com/z?=
 =?UTF-8?q?hijiantianya/yudao-cloud/issues/I8C4B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 yudao-ui-admin/src/views/system/user/index.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/yudao-ui-admin/src/views/system/user/index.vue b/yudao-ui-admin/src/views/system/user/index.vue
index c4f1b5297..50ad85a42 100644
--- a/yudao-ui-admin/src/views/system/user/index.vue
+++ b/yudao-ui-admin/src/views/system/user/index.vue
@@ -327,6 +327,9 @@ export default {
         nickname: [
           { required: true, message: "用户昵称不能为空", trigger: "blur" }
         ],
+        deptId: [
+          { required: true, message: "归属部门必选择", trigger: "blur" }
+        ],
         password: [
           { required: true, message: "用户密码不能为空", trigger: "blur" }
         ],

From 98aae5ed1a176283f937834b9074eb1f755a5b0f Mon Sep 17 00:00:00 2001
From: dongshanshan <dongshanshan@zjgtsj.wecom.work>
Date: Tue, 31 Oct 2023 16:51:56 +0800
Subject: [PATCH 051/101] =?UTF-8?q?feat:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mybatis/core/mapper/BaseMapperX.java      |   7 ++
 .../favorite/ProductFavoriteController.java   | 104 ++++++++++++++++++
 .../favorite/vo/ProductFavoriteBaseVO.java    |  21 ++++
 .../vo/ProductFavoriteBatchReqVO.java         |  21 ++++
 .../favorite/vo/ProductFavoritePageReqVO.java |  35 ++++++
 .../favorite/vo/ProductFavoriteReqVO.java     |  17 +++
 .../favorite/vo/ProductFavoriteRespVO.java    |  22 ++++
 .../favorite/ProductFavoriteConvert.java      |  19 ++++
 .../favorite/ProductFavoriteDetailDO.java     |  16 +++
 .../mysql/favorite/ProductFavoriteMapper.java |  36 ++++++
 .../favorite/ProductFavoriteService.java      |  10 +-
 .../favorite/ProductFavoriteServiceImpl.java  |   8 +-
 12 files changed, 314 insertions(+), 2 deletions(-)
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java

diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index d70c21626..2654bec52 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.baomidou.mybatisplus.extension.toolkit.Db;
 import com.github.yulichang.base.MPJBaseMapper;
+import com.github.yulichang.interfaces.MPJBaseJoin;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.Collection;
@@ -132,4 +133,10 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         Db.saveOrUpdateBatch(collection);
     }
 
+    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, @Param("resultTypeClass_Eg1sG") Class<DTO> var2, @Param("ew") MPJBaseJoin<T> queryWrapper) {
+        IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
+        selectJoinPage(mpPage, var2, queryWrapper);
+        // 转换返回
+        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+    }
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
new file mode 100644
index 000000000..e59c10827
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteBatchReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
+import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
+import cn.iocoder.yudao.module.product.service.favorite.ProductFavoriteService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 商品收藏")
+@RestController
+@RequestMapping("/product/favorite")
+@Validated
+public class ProductFavoriteController {
+
+    @Resource
+    private ProductFavoriteService productFavoriteService;
+
+    @PostMapping("/create")
+    @Operation(summary = "添加单个商品收藏")
+    @PreAuthorize("@ss.hasPermission('product:favorite:create')")
+    public CommonResult<Long> createFavorite(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
+        return success(productFavoriteService.createFavorite(reqVO.getUserId(), reqVO.getSpuId()));
+    }
+
+    @PostMapping("/create-list")
+    @Operation(summary = "添加多个商品收藏")
+    @PreAuthorize("@ss.hasPermission('product:favorite:create')")
+    public CommonResult<Boolean> createFavoriteList(@Valid @RequestBody ProductFavoriteBatchReqVO reqVO) {
+        // todo @jason:待实现;如果有已经收藏的,不用报错,忽略即可;
+        return success(Boolean.TRUE);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "取消单个商品收藏")
+    @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
+    public CommonResult<Boolean> deleteFavorite(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
+        productFavoriteService.deleteFavorite(reqVO.getUserId(), reqVO.getSpuId());
+        return success(Boolean.TRUE);
+    }
+
+    @DeleteMapping("/delete-list")
+    @Operation(summary = "取消单个商品收藏")
+    @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
+    public CommonResult<Boolean> deleteFavoriteList(@Valid @RequestBody ProductFavoriteBatchReqVO reqVO) {
+        // todo @jason:待实现
+//        productFavoriteService.deleteFavorite(getLoginUserId(), reqVO.getSpuId());
+        return success(Boolean.TRUE);
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商品收藏分页")
+    @PreAuthorize("@ss.hasPermission('product:favorite:query')")
+    public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
+        PageResult<ProductFavoriteDetailDO> favoritePage = productFavoriteService.getFavoritePageByFilter(pageVO);
+        if (CollUtil.isEmpty(favoritePage.getList())) {
+            return success(PageResult.empty());
+        }
+
+        // 得到商品 spu 信息
+        List<ProductFavoriteRespVO> favorites =  ProductFavoriteConvert.INSTANCE.convertList2admin(favoritePage.getList());
+
+        // 转换 VO 结果
+        PageResult<ProductFavoriteRespVO> pageResult = new PageResult<>(favoritePage.getTotal());
+        pageResult.setList(favorites);
+
+        return success(pageResult);
+    }
+
+    @PostMapping(value = "/exits")
+    @Operation(summary = "检查是否收藏过商品")
+    @PreAuthenticated
+    public CommonResult<Boolean> isFavoriteExists(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
+        ProductFavoriteDO favorite = productFavoriteService.getFavorite(reqVO.getUserId(), reqVO.getSpuId());
+        return success(favorite != null);
+    }
+
+    @GetMapping(value = "/get-count")
+    @Operation(summary = "获得商品收藏数量")
+    @Parameter(name = "userId", description = "用户编号", required = true)
+    @PreAuthenticated
+    public CommonResult<Long> getFavoriteCount(@RequestParam("userId") Long userId) {
+        return success(productFavoriteService.getFavoriteCount(userId));
+    }
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
new file mode 100644
index 000000000..72b2613d5
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 商品收藏 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ProductFavoriteBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5036")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
new file mode 100644
index 000000000..d779ff3a8
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+@Schema(description = "管理后台 - 商品收藏的批量 Request VO")
+@Data
+@ToString(callSuper = true)
+public class ProductFavoriteBatchReqVO extends ProductFavoriteBaseVO{
+
+    @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
+    @NotNull(message = "商品 SPU 编号数组不能为空")
+    private List<Long> spuIds;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
new file mode 100644
index 000000000..37f8cecc3
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.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 ProductFavoritePageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "5036")
+    private Long userId;
+
+    @Schema(description = "商品 SPU 编号", example = "32734")
+    private Long spuId;
+
+    @Schema(description = "收藏时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "商品名称", example = "5036")
+    private String name;
+
+    @Schema(description = "关键字", example = "5036")
+    private String keyword;
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java
new file mode 100644
index 000000000..3c2222643
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteReqVO.java
@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 商品收藏的单个 Response VO")
+@Data
+@ToString(callSuper = true)
+public class ProductFavoriteReqVO extends  ProductFavoriteBaseVO {
+
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32734")
+    @NotNull(message = "商品 SPU 编号不能为空")
+    private Long spuId;
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
new file mode 100644
index 000000000..255fc631b
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
+
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuRespVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 商品收藏 Response VO")
+@Data
+@ToString(callSuper = true)
+public class ProductFavoriteRespVO  extends ProductSpuRespVO {
+
+    @Schema(description = "userId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
+    private Long userId;
+
+    @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
+    private Long spuId;
+
+    @Schema(description = "收藏状态", example = "1")
+    private Integer favoriteStatus;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
index b15afacb2..22ffb8577 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
@@ -1,7 +1,10 @@
 package cn.iocoder.yudao.module.product.convert.favorite;
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoriteRespVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
@@ -34,4 +37,20 @@ public interface ProductFavoriteConvert {
         return resultList;
     }
 
+    @Mapping(target = "id", source = "favorite.id")
+    @Mapping(target = "userId", source = "favorite.userId")
+    @Mapping(target = "spuId", source = "favorite.spuId")
+    @Mapping(target = "createTime", source = "favorite.createTime")
+    @Mapping(target = "favoriteStatus", constant = "1")
+    ProductFavoriteRespVO convert2admin(ProductSpuDO spu, ProductFavoriteDO favorite);
+
+    default List<ProductFavoriteRespVO> convertList2admin(List<ProductFavoriteDetailDO> favorites) {
+        List<ProductFavoriteRespVO> resultList = new ArrayList<>(favorites.size());
+        for (ProductFavoriteDetailDO favorite : favorites) {
+            resultList.add(convert2admin(favorite.getSpuDO(), favorite));
+        }
+        return resultList;
+    }
+
+    PageResult<ProductFavoriteRespVO> convertPage(PageResult<ProductFavoriteDetailDO> page);
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
new file mode 100644
index 000000000..ba4d5b557
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.product.dal.dataobject.favorite;
+
+import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
+import lombok.Data;
+
+/**
+ * 商品收藏 DO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ProductFavoriteDetailDO extends ProductFavoriteDO {
+
+    ProductSpuDO spuDO;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
index 54d9d2dd6..08a5c3063 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
@@ -2,9 +2,15 @@ package cn.iocoder.yudao.module.product.dal.mysql.favorite;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
@@ -21,6 +27,36 @@ public interface ProductFavoriteMapper extends BaseMapperX<ProductFavoriteDO> {
                 .orderByDesc(ProductFavoriteDO::getId));
     }
 
+    default PageResult<ProductFavoriteDetailDO> selectPageByUserAndFields(ProductFavoritePageReqVO reqVO) {
+        MPJLambdaWrapper<ProductFavoriteDO> wrapper =  new MPJLambdaWrapper<ProductFavoriteDO>()
+                .selectAll(ProductFavoriteDO.class)
+                .eq(ProductFavoriteDO::getUserId, reqVO.getUserId())
+                .selectAssociation(ProductSpuDO.class, ProductFavoriteDetailDO::getSpuDO);
+        if(StringUtils.isNotEmpty(reqVO.getName())){
+            wrapper.likeRight(ProductSpuDO::getName, reqVO.getName());
+        }
+        if(StringUtils.isNotEmpty(reqVO.getName()) && StringUtils.isNotEmpty(reqVO.getKeyword())){
+            wrapper.or();
+        }
+        if(StringUtils.isNotEmpty(reqVO.getKeyword())){
+            wrapper.likeRight(ProductSpuDO::getKeyword, reqVO.getKeyword());
+        }
+
+        if(reqVO.getCreateTime() != null){
+            if (reqVO.getCreateTime()[0] != null && reqVO.getCreateTime()[1] != null) {
+                wrapper.between(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[0], reqVO.getCreateTime()[1]);
+            }
+            if (reqVO.getCreateTime()[0] != null) {
+                wrapper.ge(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[0]);
+            }
+            if (reqVO.getCreateTime()[1] != null) {
+                wrapper.le(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[1]);
+            }
+        }
+
+        return selectJoinPage(reqVO, ProductFavoriteDetailDO.class, wrapper);
+    }
+
     default Long selectCountByUserId(Long userId) {
         return selectCount(ProductFavoriteDO::getUserId, userId);
     }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
index 00aeddb8a..87c9854a9 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
@@ -1,8 +1,10 @@
 package cn.iocoder.yudao.module.product.service.favorite;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 
 import javax.validation.Valid;
 
@@ -37,6 +39,13 @@ public interface ProductFavoriteService {
      */
     PageResult<ProductFavoriteDO> getFavoritePage(Long userId, @Valid AppFavoritePageReqVO reqVO);
 
+    /**
+     * 分页查询用户收藏列表
+     *
+     * @param reqVO 请求 vo
+     */
+    PageResult<ProductFavoriteDetailDO> getFavoritePageByFilter(@Valid ProductFavoritePageReqVO reqVO);
+
     /**
      * 获取收藏过商品
      *
@@ -52,5 +61,4 @@ public interface ProductFavoriteService {
      * @return 数量
      */
     Long getFavoriteCount(Long userId);
-
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
index 983cbf83c..0f8d30ec0 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.product.service.favorite;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 import cn.iocoder.yudao.module.product.dal.mysql.favorite.ProductFavoriteMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -33,7 +35,6 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
         if (favorite != null) {
             throw exception(FAVORITE_EXISTS);
         }
-
         ProductFavoriteDO entity = ProductFavoriteConvert.INSTANCE.convert(userId, spuId);
         productFavoriteMapper.insert(entity);
         return entity.getId();
@@ -54,6 +55,11 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
         return productFavoriteMapper.selectPageByUserAndType(userId, reqVO);
     }
 
+    @Override
+    public PageResult<ProductFavoriteDetailDO> getFavoritePageByFilter(@Valid  ProductFavoritePageReqVO reqVO) {
+        return productFavoriteMapper.selectPageByUserAndFields(reqVO);
+    }
+
     @Override
     public ProductFavoriteDO getFavorite(Long userId, Long spuId) {
         return productFavoriteMapper.selectByUserIdAndSpuId(userId, spuId);

From c6422dc6574b975a62da0abaad2f30c9d451afb8 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Tue, 31 Oct 2023 17:24:19 +0800
Subject: [PATCH 052/101] =?UTF-8?q?=E5=AE=8C=E5=96=84=20CRM-=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=20=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90=E6=A0=A1?=
 =?UTF-8?q?=E9=AA=8C2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |   3 +-
 .../crm/enums/common/PermissionTypeEnum.java  |  31 -----
 .../crm/enums/common/TransferTypeEnum.java    |  38 ------
 .../admin/business/CrmBusinessController.http |  23 +++-
 .../business/vo/CrmTransferBusinessReqVO.java |  32 ++---
 .../convert/business/CrmBusinessConvert.java  |   8 +-
 .../crm/convert/contact/ContactConvert.java   |   8 +-
 .../crm/convert/contract/ContractConvert.java |   8 +-
 .../convert/customer/CrmCustomerConvert.java  |   8 +-
 .../permission/CrmPermissionDO.java           |  35 +++---
 .../mysql/permission/CrmPermissionMapper.java |  30 ++++-
 .../core/annotations/CrmPermission.java       |  14 ++-
 .../core/aop/CrmPermissionAspect.java         |  88 ++++++++------
 .../{CrmEnum.java => CrmBizTypeEnum.java}     |  18 ++-
 .../enums/CrmPermissionLevelEnum.java         |  50 ++++++++
 .../framework/enums/OperationTypeEnum.java    |  40 -------
 .../business/CrmBusinessServiceImpl.java      |  15 +--
 .../service/contact/ContactServiceImpl.java   |  14 +--
 .../service/contract/ContractServiceImpl.java |  14 +--
 .../customer/CrmCustomerServiceImpl.java      |  14 +--
 .../permission/CrmPermissionService.java      |  29 +++--
 .../permission/CrmPermissionServiceImpl.java  | 110 +++++++++---------
 .../permission/bo/CrmPermissionCreateBO.java  |   6 +-
 .../permission/bo/CrmPermissionUpdateBO.java  |   6 +-
 .../bo/CrmTransferPermissionReqBO.java        |  55 +++++++++
 .../bo/TransferCrmPermissionBO.java           |  53 ---------
 26 files changed, 388 insertions(+), 362 deletions(-)
 delete mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/PermissionTypeEnum.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/{CrmEnum.java => CrmBizTypeEnum.java} (55%)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 0977faa70..5aeb6ddf7 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -34,8 +34,7 @@ public interface ErrorCodeConstants {
     // ========== 客户管理 1_020_006_000 ==========
     ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
 
-    // TODO @puhui999:权限管理???
-    // ========== 客户管理 1_020_007_000 ==========
+    // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
     ErrorCode CRM_PERMISSION_DENIED = new ErrorCode(1_020_007_001, "{}操作失败,原因:没有权限");
     ErrorCode CRM_PERMISSION_MODEL_NOT_EXISTS = new ErrorCode(1_020_007_002, "{}不存在");
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/PermissionTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/PermissionTypeEnum.java
deleted file mode 100644
index 1f94ce3fd..000000000
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/PermissionTypeEnum.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package cn.iocoder.yudao.module.crm.enums.common;
-
-import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-import java.util.Arrays;
-
-@Getter
-@AllArgsConstructor
-public enum PermissionTypeEnum implements IntArrayValuable {
-
-    READONLY(1, "只读"),
-    READ_AND_WRITE(2, "读写");
-    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PermissionTypeEnum::getType).toArray();
-
-    /**
-     * 类型
-     */
-    private final Integer type;
-    /**
-     * 类型名
-     */
-    private final String name;
-
-    @Override
-    public int[] array() {
-        return ARRAYS;
-    }
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
deleted file mode 100644
index 7a4396265..000000000
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/TransferTypeEnum.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.module.crm.enums.common;
-
-import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-import java.util.Arrays;
-
-// TODO @puhui999:这个可以不用哈
-/**
- * Crm 负责人转移后原负责人的处理方式
- *
- * @author HUIHUI
- */
-@Getter
-@AllArgsConstructor
-public enum TransferTypeEnum implements IntArrayValuable {
-
-    REMOVE(1, "移除"),
-    TEAM(2, "转为团队成员");
-
-    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TransferTypeEnum::getType).toArray();
-
-    /**
-     * 类型
-     */
-    private final Integer type;
-    /**
-     * 类型名
-     */
-    private final String name;
-
-    @Override
-    public int[] array() {
-        return ARRAYS;
-    }
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http
index 631824e31..55adb4bd5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http
@@ -1,3 +1,4 @@
+### 请求 /transfer
 PUT {{baseUrl}}/crm/business/transfer
 Content-Type: application/json
 Authorization: Bearer {{token}}
@@ -8,4 +9,24 @@ tenant-id: {{adminTenentId}}
   "ownerUserId": 2,
   "transferType": 2,
   "permissionType": 2
-}
\ No newline at end of file
+}
+
+### 请求 /update
+PUT {{baseUrl}}/crm/business/update
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "id": 1,
+  "name": "2",
+  "statusTypeId": 2,
+  "statusId": 2,
+  "customerId": 1
+}
+
+### 请求 /get
+GET {{baseUrl}}/crm/business/get?id=1024
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
index 075cfb764..0d45718ff 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
@@ -1,8 +1,6 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
 
-import cn.iocoder.yudao.framework.common.validation.InEnum;
-import cn.iocoder.yudao.module.crm.enums.common.PermissionTypeEnum;
-import cn.iocoder.yudao.module.crm.enums.common.TransferTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -16,19 +14,25 @@ public class CrmTransferBusinessReqVO {
     @NotNull(message = "联系人编号不能为空")
     private Long id;
 
-    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    /**
+     * 新负责人的用户编号
+     */
     @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId;
+    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    private Long newOwnerUserId;
 
-    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @InEnum(TransferTypeEnum.class)
-    @NotNull(message = "原负责人移除方式不能为空")
-    private Integer transferType;
-
-    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @InEnum(PermissionTypeEnum.class)
-    @NotNull(message = "权限类型不能为空")
-    private Integer permissionType;
+    /**
+     * 老负责人是否加入团队,是/否
+     */
+    @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+    @NotNull(message = "老负责人是否加入团队不能为空")
+    private Boolean joinTeam;
 
+    /**
+     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer permissionLevel;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index b9ddf8496..6fa7e016e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.business;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
@@ -32,9 +32,9 @@ public interface CrmBusinessConvert {
     List<CrmBusinessExcelVO> convertList02(List<CrmBusinessDO> list);
 
     @Mappings({
-            @Mapping(target = "userId", source = "userId"),
-            @Mapping(target = "crmDataId", source = "reqVO.id")
+            @Mapping(target = "bizId", source = "reqVO.id"),
+            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    TransferCrmPermissionBO convert(CrmTransferBusinessReqVO reqVO, Long userId);
+    CrmTransferPermissionReqBO convert(CrmTransferBusinessReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index 11b549062..d866de34f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.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.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
@@ -34,9 +34,9 @@ public interface ContactConvert {
     List<ContactExcelVO> convertList02(List<ContactDO> list);
 
     @Mappings({
-            @Mapping(target = "userId", source = "userId"),
-            @Mapping(target = "crmDataId", source = "reqVO.id")
+            @Mapping(target = "bizId", source = "reqVO.id"),
+            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    TransferCrmPermissionBO convert(CrmTransferContactReqVO reqVO, Long userId);
+    CrmTransferPermissionReqBO convert(CrmTransferContactReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index bad9fa573..b81aed452 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.contract;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
@@ -34,9 +34,9 @@ public interface ContractConvert {
     List<ContractExcelVO> convertList02(List<ContractDO> list);
 
     @Mappings({
-            @Mapping(target = "userId", source = "userId"),
-            @Mapping(target = "crmDataId", source = "reqVO.id")
+            @Mapping(target = "bizId", source = "reqVO.id"),
+            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    TransferCrmPermissionBO convert(CrmTransferContractReqVO reqVO, Long userId);
+    CrmTransferPermissionReqBO convert(CrmTransferContractReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index 7072536f5..0112cd20e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.customer;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
@@ -32,9 +32,9 @@ public interface CrmCustomerConvert {
     List<CrmCustomerExcelVO> convertList02(List<CrmCustomerDO> list);
 
     @Mappings({
-            @Mapping(target = "userId", source = "userId"),
-            @Mapping(target = "crmDataId", source = "reqVO.id")
+            @Mapping(target = "bizId", source = "reqVO.id"),
+            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    TransferCrmPermissionBO convert(CrmTransferCustomerReqVO reqVO, Long userId);
+    CrmTransferPermissionReqBO convert(CrmTransferCustomerReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
index 028d41c95..b9dfabfd5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
@@ -1,23 +1,20 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.permission;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 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.*;
 
-import java.util.Set;
-
 /**
- * crm 数据权限 DO
+ * Crm 数据权限 DO
  *
  * @author HUIHUI
  */
-@TableName("crm_receivable")
-@KeySequence("crm_receivable_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@TableName("crm_permission")
+@KeySequence("crm_permission_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
@@ -32,26 +29,20 @@ public class CrmPermissionDO extends BaseDO {
     @TableId
     private Long id;
     /**
-     * 数据类型 关联 {@link CrmEnum}
+     * 数据类型,关联 {@link CrmBizTypeEnum}
      */
-    private Integer crmType;
+    private Integer bizType;
     /**
-     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
+     * 数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
      */
-    private Long crmDataId;
+    private Long bizId;
     /**
-     * 负责人的用户编号 关联 AdminUser#id
+     * 团队成员,关联 AdminUser#id
      */
-    private Long ownerUserId;
+    private Long userId;
     /**
-     * 只读权限的用户编号数组
+     * 权限级别,关联 {@link CrmPermissionLevelEnum}
      */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> rwUserIds;
+    private Integer permissionLevel;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index 53809f3eb..00640b4ba 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -3,8 +3,11 @@ package cn.iocoder.yudao.module.crm.dal.mysql.permission;
 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.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.List;
+
 /**
  * crm 数据权限 mapper
  *
@@ -13,9 +16,32 @@ import org.apache.ibatis.annotations.Mapper;
 @Mapper
 public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
 
-    default CrmPermissionDO selectByCrmTypeAndCrmDataId(Integer crmType, Long crmDataId) {
+    /**
+     * 获取用户数据权限通过 数据类型 x 某个数据 x 用户编号
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @param userId  用户编号,AdminUser#id
+     * @return Crm 数据权限
+     */
+    default CrmPermissionDO selectByBizTypeAndBizIdByUserId(Integer bizType, Long bizId, Long userId) {
         return selectOne(new LambdaQueryWrapperX<CrmPermissionDO>()
-                .eq(CrmPermissionDO::getCrmType, crmType).eq(CrmPermissionDO::getCrmDataId, crmDataId));
+                .eq(CrmPermissionDO::getBizType, bizType)
+                .eq(CrmPermissionDO::getBizId, bizId)
+                .eq(CrmPermissionDO::getUserId, userId));
+    }
+
+    /**
+     * 获取数据权限列表通过 数据类型 x 某个数据
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @return Crm 数据权限列表
+     */
+    default List<CrmPermissionDO> selectByBizTypeAndBizId(Integer bizType, Long bizId) {
+        return selectList(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getBizType, bizType)
+                .eq(CrmPermissionDO::getBizId, bizId));
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
index 9d1733b30..082b37d0e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.crm.framework.core.annotations;
 
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
@@ -24,11 +24,17 @@ public @interface CrmPermission {
     /**
      * crm 类型
      */
-    CrmEnum crmType();
+    CrmBizTypeEnum bizType();
+
+    /**
+     * 数据编号获取来源类,确保数据 id 编号在此类中,不能在父类中。
+     * 例:如果在 baseVO 中需要把 id 弄到 updateVO 中。
+     */
+    Class<?>[] getIdFor() default {};
 
     /**
      * 操作类型
      */
-    OperationTypeEnum operationType();
+    CrmPermissionLevelEnum permissionLevel();
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index 48d29c6c4..de5cf9aef 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -5,7 +5,6 @@ import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
-import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.JoinPoint;
@@ -14,11 +13,13 @@ import org.aspectj.lang.annotation.Before;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
+import java.lang.reflect.Field;
+import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_DENIED;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_MODEL_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum.*;
 
 /**
  * Crm 数据权限校验 AOP 切面
@@ -42,54 +43,67 @@ public class CrmPermissionAspect {
         return WebFrameworkUtils.getLoginUserId();
     }
 
+    private Long getBizId(JoinPoint joinPoint, CrmPermission crmPermission) throws NoSuchFieldException, IllegalAccessException {
+        Object[] args = joinPoint.getArgs();
+        for (Object arg : args) {
+            if (arg == null) {
+                continue;
+            }
+            if (ObjUtil.notEqual(arg.getClass().getName(), crmPermission.getIdFor()[0].getName())) {
+                continue;
+            }
+            // 使用反射获取id属性
+            Field idValue = arg.getClass().getDeclaredField("id");
+            // 设置字段为可访问
+            idValue.setAccessible(true);
+            return (Long) idValue.get(arg);
+        }
+        return null;
+    }
+
     @Before("@annotation(crmPermission)")
     public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
         try {
-            Integer crmType = crmPermission.crmType().getType();
-            Integer operationType = crmPermission.operationType().getType();
-            // TODO @puhui999:不一定是 id 参数噢;例如说,ContactServiceImpl 的 updateContact
-            Long id = (Long) joinPoint.getArgs()[0];// 获取操作数据的编号
+            Long bizId = crmPermission.getIdFor().length > 0 ? getBizId(joinPoint, crmPermission) : (Long) joinPoint.getArgs()[0]; // 获取操作数据的编号
+            Integer bizType = crmPermission.bizType().getType(); // 模块类型
+            Integer permissionLevel = crmPermission.permissionLevel().getLevel(); // 需要的权限级别
 
-            // 1.1 获取数据权限
-            CrmPermissionDO permission = crmPermissionService.getCrmPermissionByCrmTypeAndCrmDataId(crmType, id);
-            if (permission == null) {
-                // 不存在说明数据也不存在
-                throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, crmPermission.crmType().getName());
-            }
-            // 1.2 校验是否为公海数据
-            // TODO @puhui999:这个判断去掉比较合适哈。这里更多是业务逻辑,不算权限判断。例如说,公海的客户,只要没负责人,就可以领取了;
-            if (permission.getOwnerUserId() == null) {
+            // TODO 如果是超级管理员则直接通过
+            //if (superAdmin){
+            //    return;
+            //}
+
+            // 1. 获取数据权限
+            List<CrmPermissionDO> bizPermissions = crmPermissionService.getPermissionByBizTypeAndBizId(bizType, bizId);
+            CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, item -> ObjUtil.equal(item.getUserId(), getUserId()));
+            if (isOwner(userPermission.getPermissionLevel())) { // 校验自己是否是负责人
                 return;
             }
-            // 1.3 校验当前负责人是不是自己
-            if (ObjUtil.equal(permission.getOwnerUserId(), getUserId())) {
-                return;
+            if (isRead(permissionLevel)) { // 读权限
+                // 如果没有数据权限或没有负责人则表示此记录为公海数据所有人都有只读权限
+                if (CollUtil.isEmpty(bizPermissions) || !CollUtil.anyMatch(bizPermissions, item -> isOwner(item.getPermissionLevel()))) {
+                    return;
+                }
+                if (isRead(userPermission.getPermissionLevel())) { // 校验当前用户是否有读权限
+                    return;
+                }
+                //如果查询数据的话拥有写权限的也能查询
+                if (isWrite(userPermission.getPermissionLevel())) { // 校验当前用户是否有写权限
+                    return;
+                }
             }
-            // 1.4 TODO 校验是否为超级管理员
-
-            // 2. 校验是否有读权限
-            if (OperationTypeEnum.isRead(operationType)) {
-                // 校验该数据当前用户是否可读
-                // TODO @puhui999:直接 CollUtil.contains 就好,因为就是有某个 userId 呀
-                boolean isRead = CollUtil.contains(permission.getRoUserIds(), item -> ObjUtil.equal(item, getUserId()))
-                        || CollUtil.contains(permission.getRwUserIds(), item -> ObjUtil.equal(item, getUserId()));
-                if (isRead) {
+            if (isWrite(permissionLevel)) { // 写权限
+                if (isWrite(userPermission.getPermissionLevel())) { // 校验当前用户是否有写权限
                     return;
                 }
             }
 
-            // 3. 校验是否有编辑权限
-            if (OperationTypeEnum.isEdit(operationType)) {
-                // 校验该数据当前用户是否可读写
-                if (CollUtil.contains(permission.getRwUserIds(), item -> ObjUtil.equal(item, getUserId()))) {
-                    return;
-                }
-            }
-
-            // 4. 没通过结束,报错 {}操作失败,原因:没有权限
-            throw exception(CRM_PERMISSION_DENIED, crmPermission.crmType().getName());
+            // 2. 没通过结束,报错 {}操作失败,原因:没有权限
+            throw exception(CRM_PERMISSION_DENIED, crmPermission.bizType().getName());
         } catch (Exception ex) {
             log.error("[doBefore][crmPermission({}) 数据校验错误]", toJsonString(crmPermission), ex);
+            // TODO 报错抛个什么异常好呢
+            throw exception(CRM_PERMISSION_DENIED, crmPermission.bizType().getName());
         }
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
similarity index 55%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
index eb6110ec0..08766f63e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.crm.framework.enums;
 
 import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 
-// TODO @puhui999:可以改成 CrmBizTypeEnum,CRM 业务类型枚举
+import java.util.Arrays;
+
 /**
  * Crm 类型枚举
  *
@@ -12,7 +14,7 @@ import lombok.RequiredArgsConstructor;
  */
 @RequiredArgsConstructor
 @Getter
-public enum CrmEnum {
+public enum CrmBizTypeEnum implements IntArrayValuable {
 
     CRM_LEADS(1, "线索"),
     CRM_CUSTOMER(2, "客户"),
@@ -20,6 +22,7 @@ public enum CrmEnum {
     CRM_BUSINESS(5, "商机"),
     CRM_CONTRACT(6, "合同");
 
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmBizTypeEnum::getType).toArray();
     /**
      * 类型
      */
@@ -30,12 +33,17 @@ public enum CrmEnum {
     private final String name;
 
     public static String getNameByType(Integer type) {
-        for (CrmEnum crmEnum : CrmEnum.values()) {
-            if (ObjUtil.equal(crmEnum.type, type)) {
-                return crmEnum.name;
+        for (CrmBizTypeEnum crmBizTypeEnum : CrmBizTypeEnum.values()) {
+            if (ObjUtil.equal(crmBizTypeEnum.type, type)) {
+                return crmBizTypeEnum.name;
             }
         }
         return "";
     }
 
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
new file mode 100644
index 000000000..9df93f253
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.crm.framework.enums;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * Crm 数据权限级别枚举
+ *
+ * @author HUIHUI
+ */
+@Getter
+@AllArgsConstructor
+public enum CrmPermissionLevelEnum implements IntArrayValuable {
+
+    OWNER(1, "负责人"),
+    READ(2, "读"),
+    WRITE(3, "写");
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmPermissionLevelEnum::getLevel).toArray();
+
+    /**
+     * 级别
+     */
+    private final Integer level;
+    /**
+     * 级别名称
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static boolean isOwner(Integer level) {
+        return ObjUtil.equal(OWNER.level, level);
+    }
+
+    public static boolean isRead(Integer level) {
+        return ObjUtil.equal(READ.level, level);
+    }
+
+    public static boolean isWrite(Integer level) {
+        return ObjUtil.equal(WRITE.level, level);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
deleted file mode 100644
index 893717154..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/OperationTypeEnum.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package cn.iocoder.yudao.module.crm.framework.enums;
-
-import cn.hutool.core.util.ObjUtil;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-// TODO @puhui999:是不是可以和 PermissionTypeEnum 合并,就是 CrmPermissionEnum,负责人、读取、读写;目前阶段,不用做的特别细致;类似 linux 的 acl;
-/**
- * Crm 数据操作类型枚举
- *
- * @author HUIHUI
- */
-@RequiredArgsConstructor
-@Getter
-public enum OperationTypeEnum {
-
-    // TODO @puhui999:抽象上,就分三种,会更合理。一个 OWNER 负责人,一个 READ 读,一个 WRITE 写;
-    DELETE(1, "删除"),
-    UPDATE(2, "修改"),
-    READ(3, "查询");
-
-    /**
-     * 类型
-     */
-    private final Integer type;
-
-    /**
-     * 名称
-     */
-    private final String name;
-
-    public static boolean isRead(Integer type) {
-        return ObjUtil.equal(type, READ.getType());
-    }
-
-    public static boolean isEdit(Integer type) {
-        return ObjUtil.equal(type, UPDATE.getType()) || ObjUtil.equal(type, DELETE.getType());
-    }
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index 2885e862d..ab66448cb 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -8,8 +8,8 @@ import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import org.springframework.stereotype.Service;
@@ -46,7 +46,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         businessMapper.insert(business);
 
         // 创建数据权限
-        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmEnum.CRM_BUSINESS.getType())
+        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmBizTypeEnum.CRM_BUSINESS.getType())
                 .setCrmDataId(business.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
 
         // 返回
@@ -55,7 +55,8 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(crmType = CrmEnum.CRM_BUSINESS, operationType = OperationTypeEnum.UPDATE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, getIdFor = CrmBusinessUpdateReqVO.class,
+            permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void updateBusiness(CrmBusinessUpdateReqVO updateReqVO) {
         // 校验存在
         validateBusinessExists(updateReqVO.getId());
@@ -66,7 +67,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(crmType = CrmEnum.CRM_BUSINESS, operationType = OperationTypeEnum.DELETE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void deleteBusiness(Long id) {
         // 校验存在
         validateBusinessExists(id);
@@ -83,7 +84,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     }
 
     @Override
-    @CrmPermission(crmType = CrmEnum.CRM_BUSINESS, operationType = OperationTypeEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, permissionLevel = CrmPermissionLevelEnum.READ)
     public CrmBusinessDO getBusiness(Long id) {
         return businessMapper.selectById(id);
     }
@@ -114,7 +115,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
         // 2. 数据权限转移
         crmPermissionService.transferCrmPermission(
-                CrmBusinessConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_BUSINESS.getType()));
+                CrmBusinessConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()));
 
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index aea47ecb4..de01c2af7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -8,8 +8,8 @@ import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import org.springframework.stereotype.Service;
@@ -46,7 +46,7 @@ public class ContactServiceImpl implements ContactService {
         contactMapper.insert(contact);
 
         // 创建数据权限
-        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmEnum.CRM_BUSINESS.getType())
+        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmBizTypeEnum.CRM_BUSINESS.getType())
                 .setCrmDataId(contact.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
 
         // 返回
@@ -55,7 +55,7 @@ public class ContactServiceImpl implements ContactService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(crmType = CrmEnum.CRM_CONTACTS, operationType = OperationTypeEnum.UPDATE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void updateContact(ContactUpdateReqVO updateReqVO) {
         // 校验存在
         validateContactExists(updateReqVO.getId());
@@ -68,7 +68,7 @@ public class ContactServiceImpl implements ContactService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(crmType = CrmEnum.CRM_CONTACTS, operationType = OperationTypeEnum.DELETE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void deleteContact(Long id) {
         // 校验存在
         validateContactExists(id);
@@ -85,7 +85,7 @@ public class ContactServiceImpl implements ContactService {
     }
 
     @Override
-    @CrmPermission(crmType = CrmEnum.CRM_CONTACTS, operationType = OperationTypeEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, permissionLevel = CrmPermissionLevelEnum.READ)
     public ContactDO getContact(Long id) {
         return contactMapper.selectById(id);
     }
@@ -115,7 +115,7 @@ public class ContactServiceImpl implements ContactService {
 
         // 2. 数据权限转移
         crmPermissionService.transferCrmPermission(
-                ContactConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_CONTACTS.getType()));
+                ContactConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTACTS.getType()));
 
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index fa3940f6a..de3bfd7d6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -8,8 +8,8 @@ import cn.iocoder.yudao.module.crm.convert.contract.ContractConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contract.ContractMapper;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import org.springframework.stereotype.Service;
@@ -45,7 +45,7 @@ public class ContractServiceImpl implements ContractService {
         contractMapper.insert(contract);
 
         // 创建数据权限
-        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmEnum.CRM_CONTRACT.getType())
+        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmBizTypeEnum.CRM_CONTRACT.getType())
                 .setCrmDataId(contract.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
 
         // 返回
@@ -54,7 +54,7 @@ public class ContractServiceImpl implements ContractService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(crmType = CrmEnum.CRM_CONTRACT, operationType = OperationTypeEnum.DELETE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void updateContract(ContractUpdateReqVO updateReqVO) {
         // 校验存在
         validateContractExists(updateReqVO.getId());
@@ -65,7 +65,7 @@ public class ContractServiceImpl implements ContractService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(crmType = CrmEnum.CRM_CONTRACT, operationType = OperationTypeEnum.DELETE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void deleteContract(Long id) {
         // 校验存在
         validateContractExists(id);
@@ -82,7 +82,7 @@ public class ContractServiceImpl implements ContractService {
     }
 
     @Override
-    @CrmPermission(crmType = CrmEnum.CRM_CONTRACT, operationType = OperationTypeEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, permissionLevel = CrmPermissionLevelEnum.READ)
     public ContractDO getContract(Long id) {
         return contractMapper.selectById(id);
     }
@@ -113,7 +113,7 @@ public class ContractServiceImpl implements ContractService {
 
         // 2. 数据权限转移
         crmPermissionService.transferCrmPermission(
-                ContractConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_CONTRACT.getType()));
+                ContractConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()));
 
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index e78717b2c..a9c620d25 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -8,8 +8,8 @@ import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.OperationTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -48,7 +48,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         customerMapper.insert(customer);
 
         // 创建数据权限
-        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmEnum.CRM_CUSTOMER.getType())
+        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
                 .setCrmDataId(customer.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
 
         // 返回
@@ -57,7 +57,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(crmType = CrmEnum.CRM_CUSTOMER, operationType = OperationTypeEnum.UPDATE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void updateCustomer(CrmCustomerUpdateReqVO updateReqVO) {
         // 校验存在
         validateCustomerExists(updateReqVO.getId());
@@ -70,7 +70,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(crmType = CrmEnum.CRM_CUSTOMER, operationType = OperationTypeEnum.DELETE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void deleteCustomer(Long id) {
         // 校验存在
         validateCustomerExists(id);
@@ -87,7 +87,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     @Override
-    @CrmPermission(crmType = CrmEnum.CRM_CUSTOMER, operationType = OperationTypeEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, permissionLevel = CrmPermissionLevelEnum.READ)
     public CrmCustomerDO getCustomer(Long id) {
         return customerMapper.selectById(id);
     }
@@ -135,7 +135,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
         // 2. 数据权限转移
         crmPermissionService.transferCrmPermission(
-                CrmCustomerConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_CUSTOMER.getType()));
+                CrmCustomerConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType()));
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index 3203c06bc..c323b10da 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -2,12 +2,13 @@ package cn.iocoder.yudao.module.crm.service.permission;
 
 
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 
 import javax.validation.Valid;
+import java.util.List;
 
 /**
  * crm 数据权限 Service 接口
@@ -40,19 +41,29 @@ public interface CrmPermissionService {
     void deleteCrmPermission(Long id);
 
     /**
-     * 获得数据权限
+     * 获取用户数据权限通过 数据类型 x 某个数据 x 用户编号
      *
-     * @param crmType 数据类型 关联 {@link CrmEnum}
-     * @param crmDataId 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
-     * @return 数据权限
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @param userId  用户编号,AdminUser#id
+     * @return Crm 数据权限
      */
-    CrmPermissionDO getCrmPermissionByCrmTypeAndCrmDataId(Integer crmType, Long crmDataId);
+    CrmPermissionDO getPermissionByBizTypeAndBizIdAndUserId(Integer bizType, Long bizId, Long userId);
+
+    /**
+     * 获取数据权限列表,通过 数据类型 x 某个数据
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @return Crm 数据权限列表
+     */
+    List<CrmPermissionDO> getPermissionByBizTypeAndBizId(Integer bizType, Long bizId);
 
     /**
      * 数据权限转移
      *
-     * @param transferCrmPermissionBO 数据权限转移请求
+     * @param crmTransferPermissionReqBO 数据权限转移请求
      */
-    void transferCrmPermission(@Valid TransferCrmPermissionBO transferCrmPermissionBO);
+    void transferCrmPermission(@Valid CrmTransferPermissionReqBO crmTransferPermissionReqBO);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index 687de63ca..10457bd72 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -5,12 +5,11 @@ import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
-import cn.iocoder.yudao.module.crm.enums.common.PermissionTypeEnum;
-import cn.iocoder.yudao.module.crm.enums.common.TransferTypeEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.TransferCrmPermissionBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
@@ -18,10 +17,11 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.util.Set;
+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.framework.enums.CrmPermissionLevelEnum.isOwner;
 
 /**
  * crm 数据权限 Service 接口实现类
@@ -41,10 +41,9 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createCrmPermission(CrmPermissionCreateBO createBO) {
-        // TODO @puhui999:createDO 改成 permission,保持统一哈;
-        CrmPermissionDO createDO = CrmPermissionConvert.INSTANCE.convert(createBO);
-        crmPermissionMapper.insert(createDO);
-        return createDO.getId();
+        CrmPermissionDO permission = CrmPermissionConvert.INSTANCE.convert(createBO);
+        crmPermissionMapper.insert(permission);
+        return permission.getId();
     }
 
     @Override
@@ -64,64 +63,67 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         crmPermissionMapper.deleteById(id);
     }
 
+    @Override
+    public CrmPermissionDO getPermissionByBizTypeAndBizIdAndUserId(Integer bizType, Long bizId, Long userId) {
+        return crmPermissionMapper.selectByBizTypeAndBizIdByUserId(bizType, bizId, userId);
+    }
+
+    @Override
+    public List<CrmPermissionDO> getPermissionByBizTypeAndBizId(Integer bizType, Long bizId) {
+        return crmPermissionMapper.selectByBizTypeAndBizId(bizType, bizId);
+    }
+
     private void validateCrmPermissionExists(Long id) {
         if (crmPermissionMapper.selectById(id) == null) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
         }
     }
 
-    @Override
-    public CrmPermissionDO getCrmPermissionByCrmTypeAndCrmDataId(Integer crmType, Long crmDataId) {
-        return crmPermissionMapper.selectByCrmTypeAndCrmDataId(crmType, crmDataId);
-    }
 
-    // TODO @puhui999:参数名,是不是 transferReqBO
     @Override
-    public void transferCrmPermission(TransferCrmPermissionBO transferCrmPermissionBO) {
-        // 1.1 校验商机是否存在
-        // TODO puhui999:这里直接调用 crmPermissionMapper 的 selectByCrmTypeAndCrmDataId 方法,会更简洁一点;
-        CrmPermissionDO permission = getCrmPermissionByCrmTypeAndCrmDataId(transferCrmPermissionBO.getCrmType(),
-                transferCrmPermissionBO.getCrmDataId());
-        String crmName = CrmEnum.getNameByType(transferCrmPermissionBO.getCrmType());
-        if (permission == null) {
-            throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, crmName);
-        }
-        // 1.2 校验转移对象是否已经是该负责人
-        if (ObjUtil.equal(permission.getOwnerUserId(), permission.getOwnerUserId())) {
-            // TODO @puhui999:是不是这个错误码不太对。。。
-            throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS, crmName);
-        }
-        // 1.3 校验新负责人是否存在
-        AdminUserRespDTO user = adminUserApi.getUser(permission.getOwnerUserId());
-        if (user == null) {
-            throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, crmName);
-        }
-        // TODO 校验是否为超级管理员 || 1.4
-        // 1.4 校验是否有写权限
-        // TODO puhui999:CollUtil.contains 就够了,不用后面写个表达式;
-        if (!CollUtil.contains(permission.getRwUserIds(), id -> ObjUtil.equal(id, transferCrmPermissionBO.getUserId()))) {
+    public void transferCrmPermission(CrmTransferPermissionReqBO transferReqBO) {
+        // 1. 校验数据权限-是否是负责人,只有负责人才可以转移
+        CrmPermissionDO oldPermission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(transferReqBO.getBizType(),
+                transferReqBO.getBizId(), transferReqBO.getUserId());
+        String crmName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType());
+        // TODO 校验是否为超级管理员 || 1
+        if (oldPermission == null || !isOwner(oldPermission.getPermissionLevel())) {
             throw exception(CRM_PERMISSION_DENIED, crmName);
         }
 
-        // 2. 权限转移
-        CrmPermissionDO updateCrmPermission = new CrmPermissionDO().setId(permission.getId())
-                .setOwnerUserId(transferCrmPermissionBO.getOwnerUserId());
-        if (ObjUtil.equal(TransferTypeEnum.TEAM.getType(), transferCrmPermissionBO.getTransferType())) {
-            if (ObjUtil.equal(PermissionTypeEnum.READONLY.getType(), transferCrmPermissionBO.getPermissionType())) {
-                Set<Long> roUserIds = permission.getRoUserIds();
-                roUserIds.add(permission.getOwnerUserId()); // 老负责人加入团队有只读权限
-                updateCrmPermission.setRoUserIds(roUserIds);
-            }
-            if (ObjUtil.equal(PermissionTypeEnum.READ_AND_WRITE.getType(), transferCrmPermissionBO.getPermissionType())) {
-                Set<Long> rwUserIds = permission.getRwUserIds();
-                rwUserIds.add(permission.getOwnerUserId()); // 老负责人加入团队有读写权限
-                updateCrmPermission.setRoUserIds(rwUserIds);
-            }
+        // 2. 校验转移对象是否已经是该负责人
+        if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) {
+            throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, crmName);
+        }
+        // 2.1 校验新负责人是否存在
+        AdminUserRespDTO user = adminUserApi.getUser(transferReqBO.getNewOwnerUserId());
+        if (user == null) {
+            throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS, crmName);
         }
-        crmPermissionMapper.updateById(updateCrmPermission);
 
-        // 3. TODO 记录机转移日志
-        // TODO @puhui999:是不是交给业务记录哈;
+        // 3. 权限转移
+        List<CrmPermissionDO> permissions = crmPermissionMapper.selectByBizTypeAndBizId(
+                transferReqBO.getBizType(), transferReqBO.getBizId()); // 获取所有团队成员
+        // 3.1 校验新负责人是否在团队成员中
+        CrmPermissionDO permission = CollUtil.findOne(permissions,
+                item -> ObjUtil.equal(item.getUserId(), transferReqBO.getNewOwnerUserId()));
+        if (permission == null) { // 不存在则以负责人的级别加入这个团队
+            crmPermissionMapper.insert(new CrmPermissionDO().setBizType(transferReqBO.getBizType())
+                    .setBizId(transferReqBO.getBizId()).setUserId(transferReqBO.getNewOwnerUserId())
+                    .setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+
+        } else { // 存在则修改权限级别
+            crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId())
+                    .setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+        }
+
+        // 4. 老负责人处理
+        if (transferReqBO.getJoinTeam()) { // 加入团队
+            crmPermissionMapper.updateById(new CrmPermissionDO().setId(oldPermission.getId())
+                    .setPermissionLevel(transferReqBO.getPermissionLevel())); // 设置加入团队后的级别
+            return;
+        }
+        crmPermissionMapper.deleteById(oldPermission.getId()); // 移除
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
index e5dd5d6e9..21d28a79b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.module.crm.service.permission.bo;
 
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import lombok.Data;
 
 import javax.validation.constraints.NotNull;
@@ -18,12 +18,12 @@ public class CrmPermissionCreateBO {
     // TODO @puhui999:如果是关联字段,换一行写它的注释;不然看着略乱哈
 
     /**
-     * Crm 类型 关联 {@link CrmEnum}
+     * Crm 类型 关联 {@link CrmBizTypeEnum}
      */
     @NotNull(message = "Crm 类型不能为空")
     private Integer crmType;
     /**
-     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
+     * 数据编号 关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
      */
     @NotNull(message = "Crm 数据编号不能为空")
     private Long crmDataId;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
index e8544a611..da3dd959d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.permission.bo;
 
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import lombok.Data;
 
 import javax.validation.constraints.NotNull;
@@ -24,12 +24,12 @@ public class CrmPermissionUpdateBO {
     private Long id;
 
     /**
-     * Crm 类型 关联 {@link CrmEnum}
+     * Crm 类型 关联 {@link CrmBizTypeEnum}
      */
     @NotNull(message = "Crm 类型不能为空")
     private Integer crmType;
     /**
-     * 数据编号 关联 {@link CrmEnum} 对应模块 DO#getId()
+     * 数据编号 关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
      */
     @NotNull(message = "Crm 数据编号不能为空")
     private Long crmDataId;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java
new file mode 100644
index 000000000..3dce1cb57
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java
@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.crm.service.permission.bo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 数据权限转移 Request BO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CrmTransferPermissionReqBO {
+
+    /**
+     * 当前登录用户编号
+     */
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    /**
+     * Crm 类型
+     */
+    @NotNull(message = "Crm 类型不能为空")
+    @InEnum(CrmBizTypeEnum.class)
+    private Integer bizType;
+
+    /**
+     * 数据编号
+     */
+    @NotNull(message = "Crm 数据编号不能为空")
+    private Long bizId;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人是否加入团队,是/否
+     */
+    @NotNull(message = "老负责人是否加入团队不能为空")
+    private Boolean joinTeam;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    private Integer permissionLevel;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java
deleted file mode 100644
index 246439bcb..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/TransferCrmPermissionBO.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package cn.iocoder.yudao.module.crm.service.permission.bo;
-
-import cn.iocoder.yudao.module.crm.enums.common.PermissionTypeEnum;
-import cn.iocoder.yudao.module.crm.enums.common.TransferTypeEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmEnum;
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-
-// TODO @puhui999:CrmTransferPermissionReqBO,一个是 Crm 前缀,一个 Req 表示入参
-/**
- * 数据权限转移 BO
- *
- * @author HUIHUI
- */
-@Data
-public class TransferCrmPermissionBO {
-
-    // TODO @puhui999:参数的注释
-    @NotNull(message = "用户编号不能为空")
-    private Long userId;
-
-    // TODO @puhui999:bizType
-    /**
-     * Crm 类型 关联 {@link CrmEnum} TODO 这种不用再写关联了,直接 @InEnum 参数校验
-     */
-    @NotNull(message = "Crm 类型不能为空")
-    private Integer crmType;
-
-    // TODO @puhui999:bizId
-    /**
-     * 数据编号
-     */
-    @NotNull(message = "Crm 数据编号不能为空")
-    private Long crmDataId;
-
-    // TODO @puhui999:要不这里改成 newOwnerUserId;然后,transferType 和 permissionType,合并成 oldOwnerPermission(空就是移除)
-    @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId;
-
-    /**
-     * 原负责人移除方式, 关联 {@link TransferTypeEnum} TODO 这种不用再写关联了,直接 @InEnum 参数校验
-     */
-    @NotNull(message = "原负责人移除方式不能为空")
-    private Integer transferType;
-
-    /**
-     * 权限类型, 关联 {@link PermissionTypeEnum}
-     */
-    @NotNull(message = "权限类型不能为空")
-    private Integer permissionType;
-
-}

From a385a37c4af29eaea3d12a4310a4dae398f0b415 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Tue, 31 Oct 2023 17:42:44 +0800
Subject: [PATCH 053/101] =?UTF-8?q?=E5=AE=8C=E5=96=84=20CRM-=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=20=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90=E6=A0=A1?=
 =?UTF-8?q?=E9=AA=8C2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../permission/CrmPermissionConvert.java      |  8 +--
 .../core/aop/CrmPermissionAspect.java         |  2 +-
 .../business/CrmBusinessServiceImpl.java      |  7 +--
 .../service/contact/ContactServiceImpl.java   | 10 ++--
 .../service/contract/ContractServiceImpl.java | 10 ++--
 .../customer/CrmCustomerServiceImpl.java      | 12 +++--
 .../permission/CrmPermissionService.java      | 10 ++--
 .../permission/CrmPermissionServiceImpl.java  | 10 ++--
 .../permission/bo/CrmPermissionCreateBO.java  | 44 ----------------
 .../bo/CrmPermissionCreateReqBO.java          | 45 ++++++++++++++++
 .../permission/bo/CrmPermissionUpdateBO.java  | 49 -----------------
 .../bo/CrmPermissionUpdateReqBO.java          | 52 +++++++++++++++++++
 12 files changed, 136 insertions(+), 123 deletions(-)
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
index 0fc6f61cc..22e49b61f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.crm.convert.permission;
 
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
@@ -16,8 +16,8 @@ public interface CrmPermissionConvert {
 
     CrmPermissionConvert INSTANCE = Mappers.getMapper(CrmPermissionConvert.class);
 
-    CrmPermissionDO convert(CrmPermissionCreateBO createBO);
+    CrmPermissionDO convert(CrmPermissionCreateReqBO createBO);
 
-    CrmPermissionDO convert(CrmPermissionUpdateBO updateBO);
+    CrmPermissionDO convert(CrmPermissionUpdateReqBO updateBO);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index de5cf9aef..7635d6333 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -81,7 +81,7 @@ public class CrmPermissionAspect {
             }
             if (isRead(permissionLevel)) { // 读权限
                 // 如果没有数据权限或没有负责人则表示此记录为公海数据所有人都有只读权限
-                if (CollUtil.isEmpty(bizPermissions) || !CollUtil.anyMatch(bizPermissions, item -> isOwner(item.getPermissionLevel()))) {
+                if (!CollUtil.anyMatch(bizPermissions, item -> isOwner(item.getPermissionLevel()))) {
                     return;
                 }
                 if (isRead(userPermission.getPermissionLevel())) { // 校验当前用户是否有读权限
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index ab66448cb..c3a262253 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -46,8 +46,8 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         businessMapper.insert(business);
 
         // 创建数据权限
-        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmBizTypeEnum.CRM_BUSINESS.getType())
-                .setCrmDataId(business.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType())
+                .setBizId(business.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
 
         // 返回
         return business.getId();
@@ -117,6 +117,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         crmPermissionService.transferCrmPermission(
                 CrmBusinessConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()));
 
+        // 3. TODO 记录转移日志
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index de01c2af7..b1ca89e78 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -46,8 +46,8 @@ public class ContactServiceImpl implements ContactService {
         contactMapper.insert(contact);
 
         // 创建数据权限
-        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmBizTypeEnum.CRM_BUSINESS.getType())
-                .setCrmDataId(contact.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CONTACTS.getType())
+                .setBizId(contact.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
 
         // 返回
         return contact.getId();
@@ -55,7 +55,8 @@ public class ContactServiceImpl implements ContactService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, getIdFor = ContactUpdateReqVO.class,
+            permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void updateContact(ContactUpdateReqVO updateReqVO) {
         // 校验存在
         validateContactExists(updateReqVO.getId());
@@ -117,6 +118,7 @@ public class ContactServiceImpl implements ContactService {
         crmPermissionService.transferCrmPermission(
                 ContactConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTACTS.getType()));
 
+        // 3. TODO 记录转移日志
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index de3bfd7d6..f053821b0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -45,8 +45,8 @@ public class ContractServiceImpl implements ContractService {
         contractMapper.insert(contract);
 
         // 创建数据权限
-        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmBizTypeEnum.CRM_CONTRACT.getType())
-                .setCrmDataId(contract.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType())
+                .setBizId(contract.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
 
         // 返回
         return contract.getId();
@@ -54,7 +54,8 @@ public class ContractServiceImpl implements ContractService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, getIdFor = ContractUpdateReqVO.class,
+            permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void updateContract(ContractUpdateReqVO updateReqVO) {
         // 校验存在
         validateContractExists(updateReqVO.getId());
@@ -115,6 +116,7 @@ public class ContractServiceImpl implements ContractService {
         crmPermissionService.transferCrmPermission(
                 ContractConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()));
 
+        // 3. TODO 记录转移日志
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index a9c620d25..7e23d4de6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -48,8 +48,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         customerMapper.insert(customer);
 
         // 创建数据权限
-        crmPermissionService.createCrmPermission(new CrmPermissionCreateBO().setCrmType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
-                .setCrmDataId(customer.getId()).setOwnerUserId(userId)); // 设置当前操作的人为负责人
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
+                .setBizId(customer.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
 
         // 返回
         return customer.getId();
@@ -57,7 +57,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, getIdFor = CrmCustomerUpdateReqVO.class,
+            permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void updateCustomer(CrmCustomerUpdateReqVO updateReqVO) {
         // 校验存在
         validateCustomerExists(updateReqVO.getId());
@@ -112,6 +113,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     // TODO wanwan:service 接口已经注释,实现类就不需要了。
+
     /**
      * 校验客户是否存在
      *
@@ -136,6 +138,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         // 2. 数据权限转移
         crmPermissionService.transferCrmPermission(
                 CrmCustomerConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType()));
+
+        // 3. TODO 记录转移日志
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index c323b10da..e85c8c1b5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.crm.service.permission;
 
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 
 import javax.validation.Valid;
@@ -24,21 +24,21 @@ public interface CrmPermissionService {
      * @param createBO 创建信息
      * @return 编号
      */
-    Long createCrmPermission(@Valid CrmPermissionCreateBO createBO);
+    Long createPermission(@Valid CrmPermissionCreateReqBO createBO);
 
     /**
      * 更新数据权限
      *
      * @param updateBO 更新信息
      */
-    void updateCrmPermission(@Valid CrmPermissionUpdateBO updateBO);
+    void updatePermission(@Valid CrmPermissionUpdateReqBO updateBO);
 
     /**
      * 删除数据权限
      *
      * @param id 编号
      */
-    void deleteCrmPermission(Long id);
+    void deletePermission(Long id);
 
     /**
      * 获取用户数据权限通过 数据类型 x 某个数据 x 用户编号
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index 10457bd72..b962bcdda 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -40,7 +40,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public Long createCrmPermission(CrmPermissionCreateBO createBO) {
+    public Long createPermission(CrmPermissionCreateReqBO createBO) {
         CrmPermissionDO permission = CrmPermissionConvert.INSTANCE.convert(createBO);
         crmPermissionMapper.insert(permission);
         return permission.getId();
@@ -48,7 +48,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void updateCrmPermission(CrmPermissionUpdateBO updateBO) {
+    public void updatePermission(CrmPermissionUpdateReqBO updateBO) {
         validateCrmPermissionExists(updateBO.getId());
         // 更新操作
         CrmPermissionDO updateDO = CrmPermissionConvert.INSTANCE.convert(updateBO);
@@ -57,7 +57,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void deleteCrmPermission(Long id) {
+    public void deletePermission(Long id) {
         validateCrmPermissionExists(id);
         // 删除
         crmPermissionMapper.deleteById(id);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
deleted file mode 100644
index 21d28a79b..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateBO.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package cn.iocoder.yudao.module.crm.service.permission.bo;
-
-import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-import java.util.Set;
-
-// TODO @puhui999:,一个是 Crm 前缀,一个 Req 表示入参
-/**
- * crm 数据权限 Create BO
- *
- * @author HUIHUI
- */
-@Data
-public class CrmPermissionCreateBO {
-
-    // TODO @puhui999:如果是关联字段,换一行写它的注释;不然看着略乱哈
-
-    /**
-     * Crm 类型 关联 {@link CrmBizTypeEnum}
-     */
-    @NotNull(message = "Crm 类型不能为空")
-    private Integer crmType;
-    /**
-     * 数据编号 关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
-     */
-    @NotNull(message = "Crm 数据编号不能为空")
-    private Long crmDataId;
-
-    /**
-     * 负责人的用户编号 关联 AdminUser#id, null 则为公海数据
-     */
-    private Long ownerUserId;
-    /**
-     * 只读权限的用户编号数组
-     */
-    private Set<Long> roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    private Set<Long> rwUserIds;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
new file mode 100644
index 000000000..cff112b7e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.crm.service.permission.bo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * crm 数据权限 Create Req BO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CrmPermissionCreateReqBO {
+
+    /**
+     * 当前登录用户编号
+     */
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    /**
+     * Crm 类型
+     */
+    @NotNull(message = "Crm 类型不能为空")
+    @InEnum(CrmBizTypeEnum.class)
+    private Integer bizType;
+
+    /**
+     * 数据编号
+     */
+    @NotNull(message = "Crm 数据编号不能为空")
+    private Long bizId;
+
+    /**
+     * 权限级别
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @NotNull(message = "权限级别不能为空")
+    @InEnum(CrmPermissionLevelEnum.class)
+    private Integer permissionLevel;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
deleted file mode 100644
index da3dd959d..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateBO.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package cn.iocoder.yudao.module.crm.service.permission.bo;
-
-import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-import java.util.Set;
-
-// TODO @puhui999:,一个是 Crm 前缀,一个 Req 表示入参
-/**
- * crm 数据权限 Update BO
- *
- * @author HUIHUI
- */
-@Data
-public class CrmPermissionUpdateBO {
-
-    // TODO @puhui999:id 和 crmType + crmDataId 是不是重叠了;
-    /**
-     * 数据权限编号 {@link CrmPermissionDO#getId()}
-     */
-    @NotNull(message = "Crm 数据权限编号不能为空")
-    private Long id;
-
-    /**
-     * Crm 类型 关联 {@link CrmBizTypeEnum}
-     */
-    @NotNull(message = "Crm 类型不能为空")
-    private Integer crmType;
-    /**
-     * 数据编号 关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
-     */
-    @NotNull(message = "Crm 数据编号不能为空")
-    private Long crmDataId;
-    /**
-     * 负责人的用户编号 关联 AdminUser#id, null 则为公海数据
-     */
-    private Long ownerUserId;
-    /**
-     * 只读权限的用户编号数组
-     */
-    private Set<Long> roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    private Set<Long> rwUserIds;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
new file mode 100644
index 000000000..72f94c46e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.service.permission.bo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * crm 数据权限 Update Req BO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CrmPermissionUpdateReqBO {
+
+    /**
+     * 数据权限编号 {@link CrmPermissionDO#getId()}
+     */
+    @NotNull(message = "Crm 数据权限编号不能为空")
+    private Long id;
+
+    /**
+     * 当前登录用户编号
+     */
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    /**
+     * Crm 类型
+     */
+    @NotNull(message = "Crm 类型不能为空")
+    @InEnum(CrmBizTypeEnum.class)
+    private Integer bizType;
+
+    /**
+     * 数据编号
+     */
+    @NotNull(message = "Crm 数据编号不能为空")
+    private Long bizId;
+
+    /**
+     * 权限级别
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @NotNull(message = "权限级别不能为空")
+    @InEnum(CrmPermissionLevelEnum.class)
+    private Integer permissionLevel;
+
+}

From 91dd61edc0c7a8b9db590892e8ee60a06096f731 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Wed, 1 Nov 2023 10:21:56 +0800
Subject: [PATCH 054/101] =?UTF-8?q?=E5=AE=8C=E5=96=84=20review=20=E6=8F=90?=
 =?UTF-8?q?=E5=88=B0=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/business/CrmBusinessController.java |  3 +-
 ...qVO.java => CrmBusinessTransferReqVO.java} |  4 +-
 .../admin/contact/ContactController.java      |  2 +-
 .../contact/vo/CrmContactTransferReqVO.java   | 38 +++++++++++++++++++
 .../contact/vo/CrmTransferContactReqVO.java   | 28 --------------
 .../admin/contract/ContractController.java    |  2 +-
 .../contract/vo/CrmContractTransferReqVO.java | 38 +++++++++++++++++++
 .../contract/vo/CrmTransferContractReqVO.java | 29 --------------
 .../admin/customer/CrmCustomerController.java |  2 +-
 .../customer/vo/CrmCustomerTransferReqVO.java | 38 +++++++++++++++++++
 .../customer/vo/CrmTransferCustomerReqVO.java | 28 --------------
 .../convert/business/CrmBusinessConvert.java  |  2 +-
 .../crm/convert/contact/ContactConvert.java   |  2 +-
 .../crm/convert/contract/ContractConvert.java |  2 +-
 .../convert/customer/CrmCustomerConvert.java  |  2 +-
 .../permission/CrmPermissionDO.java           |  2 +
 .../mysql/permission/CrmPermissionMapper.java | 13 +++++++
 .../core/aop/CrmPermissionAspect.java         |  4 +-
 .../service/business/CrmBusinessService.java  |  2 +-
 .../business/CrmBusinessServiceImpl.java      |  2 +-
 .../crm/service/contact/ContactService.java   |  2 +-
 .../service/contact/ContactServiceImpl.java   |  2 +-
 .../crm/service/contract/ContractService.java |  2 +-
 .../service/contract/ContractServiceImpl.java |  2 +-
 .../service/customer/CrmCustomerService.java  |  2 +-
 .../customer/CrmCustomerServiceImpl.java      |  2 +-
 .../permission/CrmPermissionService.java      | 10 ++++-
 .../permission/CrmPermissionServiceImpl.java  |  5 +++
 28 files changed, 164 insertions(+), 106 deletions(-)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/{CrmTransferBusinessReqVO.java => CrmBusinessTransferReqVO.java} (88%)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index efff25372..eb83fa743 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -91,9 +91,10 @@ public class CrmBusinessController {
     @PutMapping("/transfer")
     @Operation(summary = "商机转移")
     @PreAuthorize("@ss.hasPermission('crm:business:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferBusinessReqVO reqVO) {
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmBusinessTransferReqVO reqVO) {
         businessService.transferBusiness(reqVO, getLoginUserId());
         return success(true);
     }
 
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
similarity index 88%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
index 0d45718ff..aa85404a7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmTransferBusinessReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
@@ -8,7 +8,7 @@ import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 商机转移 Request VO")
 @Data
-public class CrmTransferBusinessReqVO {
+public class CrmBusinessTransferReqVO {
 
     @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     @NotNull(message = "联系人编号不能为空")
@@ -18,7 +18,7 @@ public class CrmTransferBusinessReqVO {
      * 新负责人的用户编号
      */
     @NotNull(message = "新负责人的用户编号不能为空")
-    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
index cda6b29b9..5d92abc45 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -100,7 +100,7 @@ public class ContactController {
     @PutMapping("/transfer")
     @Operation(summary = "联系人转移")
     @PreAuthorize("@ss.hasPermission('crm:contact:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferContactReqVO reqVO) {
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContactTransferReqVO reqVO) {
         contactService.transferContact(reqVO, getLoginUserId());
         return success(true);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
new file mode 100644
index 000000000..fa8940ea3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 联系人转移 Request VO")
+@Data
+public class CrmContactTransferReqVO {
+
+    @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @NotNull(message = "新负责人的用户编号不能为空")
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人是否加入团队,是/否
+     */
+    @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+    @NotNull(message = "老负责人是否加入团队不能为空")
+    private Boolean joinTeam;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer permissionLevel;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
deleted file mode 100644
index 1e16d2d20..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmTransferContactReqVO.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-
-@Schema(description = "管理后台 - CRM 联系人转移 Request VO")
-@Data
-public class CrmTransferContactReqVO {
-
-    @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "联系人编号不能为空")
-    private Long id;
-
-    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId;
-
-    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "原负责人移除方式不能为空")
-    private Integer transferType;
-
-    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "权限类型不能为空")
-    private Integer permissionType;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
index d875b21e3..1028929db 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
@@ -90,7 +90,7 @@ public class ContractController {
     @PutMapping("/transfer")
     @Operation(summary = "合同转移")
     @PreAuthorize("@ss.hasPermission('crm:contract:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferContractReqVO reqVO) {
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContractTransferReqVO reqVO) {
         contractService.transferContract(reqVO, getLoginUserId());
         return success(true);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
new file mode 100644
index 000000000..cf3e0effe
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 合同转移 Request VO")
+@Data
+public class CrmContractTransferReqVO {
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @NotNull(message = "新负责人的用户编号不能为空")
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人是否加入团队,是/否
+     */
+    @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+    @NotNull(message = "老负责人是否加入团队不能为空")
+    private Boolean joinTeam;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer permissionLevel;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
deleted file mode 100644
index a987c9f89..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmTransferContractReqVO.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-
-// TODO @puhui999:CrmContractTransferReqVO,模块名字要放前面;看看还有没其它类似的
-@Schema(description = "管理后台 - CRM 合同转移 Request VO")
-@Data
-public class CrmTransferContractReqVO {
-
-    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "合同编号不能为空")
-    private Long id;
-
-    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId;
-
-    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "原负责人移除方式不能为空")
-    private Integer transferType;
-
-    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "权限类型不能为空")
-    private Integer permissionType;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index ff144ffbf..c44c12b70 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -90,7 +90,7 @@ public class CrmCustomerController {
     @PutMapping("/transfer")
     @Operation(summary = "客户转移")
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmTransferCustomerReqVO reqVO) {
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmCustomerTransferReqVO reqVO) {
         customerService.transferCustomer(reqVO, getLoginUserId());
         return success(true);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
new file mode 100644
index 000000000..20f895197
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 客户转移 Request VO")
+@Data
+public class CrmCustomerTransferReqVO {
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @NotNull(message = "新负责人的用户编号不能为空")
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人是否加入团队,是/否
+     */
+    @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
+    @NotNull(message = "老负责人是否加入团队不能为空")
+    private Boolean joinTeam;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer permissionLevel;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java
deleted file mode 100644
index 78ca15aca..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmTransferCustomerReqVO.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import javax.validation.constraints.NotNull;
-
-@Schema(description = "管理后台 - CRM 客户转移 Request VO")
-@Data
-public class CrmTransferCustomerReqVO {
-
-    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "客户编号不能为空")
-    private Long id;
-
-    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "新负责人的用户编号不能为空")
-    private Long ownerUserId;
-
-    @Schema(description = "原负责人移除方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "原负责人移除方式不能为空")
-    private Integer transferType;
-
-    @Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
-    @NotNull(message = "权限类型不能为空")
-    private Integer permissionType;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index 6fa7e016e..3c27c0ce3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -35,6 +35,6 @@ public interface CrmBusinessConvert {
             @Mapping(target = "bizId", source = "reqVO.id"),
             @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    CrmTransferPermissionReqBO convert(CrmTransferBusinessReqVO reqVO, Long userId);
+    CrmTransferPermissionReqBO convert(CrmBusinessTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index d866de34f..b4cc69963 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -37,6 +37,6 @@ public interface ContactConvert {
             @Mapping(target = "bizId", source = "reqVO.id"),
             @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    CrmTransferPermissionReqBO convert(CrmTransferContactReqVO reqVO, Long userId);
+    CrmTransferPermissionReqBO convert(CrmContactTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index b81aed452..7f629ef8c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -37,6 +37,6 @@ public interface ContractConvert {
             @Mapping(target = "bizId", source = "reqVO.id"),
             @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    CrmTransferPermissionReqBO convert(CrmTransferContractReqVO reqVO, Long userId);
+    CrmTransferPermissionReqBO convert(CrmContractTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index 0112cd20e..6aa86218c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -35,6 +35,6 @@ public interface CrmCustomerConvert {
             @Mapping(target = "bizId", source = "reqVO.id"),
             @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    CrmTransferPermissionReqBO convert(CrmTransferCustomerReqVO reqVO, Long userId);
+    CrmTransferPermissionReqBO convert(CrmCustomerTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
index b9dfabfd5..2601b5d09 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
@@ -42,6 +42,8 @@ public class CrmPermissionDO extends BaseDO {
     private Long userId;
     /**
      * 权限级别,关联 {@link CrmPermissionLevelEnum}
+     * 如果为公海数据的话会干掉此数据的负责人,领取人则上位负责人
+     * 例:客户放入公海后会干掉团队成员中的负责人,而其他团队成员则不受影响
      */
     private Integer permissionLevel;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index 00640b4ba..c2db6acbf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -44,4 +44,17 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 .eq(CrmPermissionDO::getBizId, bizId));
     }
 
+    /**
+     * 获取数据权限列表通过 数据类型 x 用户编号。如果
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param userId
+     * @return Crm 数据权限列表
+     */
+    default List<CrmPermissionDO> selectByBizTypeAndUserId(Integer bizType, Long userId) {
+        return selectList(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getBizType, bizType)
+                .eq(CrmPermissionDO::getUserId, userId));
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index 7635d6333..ee921d2c7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -80,8 +80,8 @@ public class CrmPermissionAspect {
                 return;
             }
             if (isRead(permissionLevel)) { // 读权限
-                // 如果没有数据权限或没有负责人则表示此记录为公海数据所有人都有只读权限
-                if (!CollUtil.anyMatch(bizPermissions, item -> isOwner(item.getPermissionLevel()))) {
+                // 如果没有数据权限或没有负责人则表示此记录为公海数据所有人都有只读权限可以领取成为负责人(团队成员领取的)
+                if (CollUtil.isEmpty(bizPermissions) || !CollUtil.anyMatch(bizPermissions, item -> isOwner(item.getPermissionLevel()))) {
                     return;
                 }
                 if (isRead(userPermission.getPermissionLevel())) { // 校验当前用户是否有读权限
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index 63baad383..aeee4b426 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -76,6 +76,6 @@ public interface CrmBusinessService {
      * @param reqVO  请求
      * @param userId 用户编号
      */
-    void transferBusiness(CrmTransferBusinessReqVO reqVO, Long userId);
+    void transferBusiness(CrmBusinessTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index c3a262253..b3e1b9790 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -109,7 +109,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void transferBusiness(CrmTransferBusinessReqVO reqVO, Long userId) {
+    public void transferBusiness(CrmBusinessTransferReqVO reqVO, Long userId) {
         // 1 校验商机是否存在
         validateBusinessExists(reqVO.getId());
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
index 6da77138e..ecb6fd577 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
@@ -76,6 +76,6 @@ public interface ContactService {
      * @param reqVO  请求
      * @param userId 用户编号
      */
-    void transferContact(CrmTransferContactReqVO reqVO, Long userId);
+    void transferContact(CrmContactTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index b1ca89e78..550829fbe 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -110,7 +110,7 @@ public class ContactServiceImpl implements ContactService {
     }
 
     @Override
-    public void transferContact(CrmTransferContactReqVO reqVO, Long userId) {
+    public void transferContact(CrmContactTransferReqVO reqVO, Long userId) {
         // 1 校验联系人是否存在
         validateContactExists(reqVO.getId());
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
index 8cf41c152..201684bd8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractService.java
@@ -76,6 +76,6 @@ public interface ContractService {
      * @param reqVO  请求
      * @param userId 用户编号
      */
-    void transferContract(CrmTransferContractReqVO reqVO, Long userId);
+    void transferContract(CrmContractTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index f053821b0..c4b144462 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -108,7 +108,7 @@ public class ContractServiceImpl implements ContractService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void transferContract(CrmTransferContractReqVO reqVO, Long userId) {
+    public void transferContract(CrmContractTransferReqVO reqVO, Long userId) {
         // 1 校验合同是否存在
         validateContractExists(reqVO.getId());
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index 576a9bcbc..5f64142b2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -84,6 +84,6 @@ public interface CrmCustomerService {
      * @param reqVO  请求
      * @param userId 用户编号
      */
-    void transferCustomer(CrmTransferCustomerReqVO reqVO, Long userId);
+    void transferCustomer(CrmCustomerTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 7e23d4de6..87d23a25f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -131,7 +131,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void transferCustomer(CrmTransferCustomerReqVO reqVO, Long userId) {
+    public void transferCustomer(CrmCustomerTransferReqVO reqVO, Long userId) {
         // 1. 校验合同是否存在
         validateCustomer(reqVO.getId());
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index e85c8c1b5..5e2daef08 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -17,7 +17,6 @@ import java.util.List;
  */
 public interface CrmPermissionService {
 
-    // TODO @puhui999:方法名上,不用 Crm
     /**
      * 创建数据权限
      *
@@ -59,6 +58,15 @@ public interface CrmPermissionService {
      */
     List<CrmPermissionDO> getPermissionByBizTypeAndBizId(Integer bizType, Long bizId);
 
+    /**
+     * 获取数据权限列表,通过 数据类型 x 用户编号
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param userId  用户编号
+     * @return Crm 数据权限列表
+     */
+    List<CrmPermissionDO> getPermissionByBizTypeAndUserId(Integer bizType, Long userId);
+
     /**
      * 数据权限转移
      *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index b962bcdda..8d4047741 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -73,6 +73,11 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         return crmPermissionMapper.selectByBizTypeAndBizId(bizType, bizId);
     }
 
+    @Override
+    public List<CrmPermissionDO> getPermissionByBizTypeAndUserId(Integer bizType, Long userId) {
+        return crmPermissionMapper.selectByBizTypeAndUserId(bizType, userId);
+    }
+
     private void validateCrmPermissionExists(Long id) {
         if (crmPermissionMapper.selectById(id) == null) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);

From 7a8ffd9ccb6b8ee434ea79f917aa249b2a9a4694 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Wed, 1 Nov 2023 11:28:05 +0800
Subject: [PATCH 055/101] =?UTF-8?q?CRM-=E5=95=86=E6=9C=BA=EF=BC=9A?=
 =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=8E=B7=E5=8F=96=E6=9D=83=E9=99=90=E8=BF=87?=
 =?UTF-8?q?=E6=BB=A4=E5=90=8E=E7=9A=84=E5=88=86=E9=A1=B5=E6=95=B0=E6=8D=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/business/CrmBusinessController.java |  3 +-
 .../convert/business/CrmBusinessConvert.java  |  3 ++
 .../permission/CrmPermissionDO.java           | 10 ++++--
 .../dal/mysql/business/CrmBusinessMapper.java |  7 ++--
 .../mysql/permission/CrmPermissionMapper.java |  8 +++++
 .../core/aop/CrmPermissionAspect.java         |  3 +-
 .../service/business/CrmBusinessService.java  |  3 +-
 .../business/CrmBusinessServiceImpl.java      | 19 +++++++++--
 .../service/contact/ContactServiceImpl.java   |  2 +-
 .../service/contract/ContractServiceImpl.java |  2 +-
 .../customer/CrmCustomerServiceImpl.java      |  2 +-
 .../permission/CrmPermissionService.java      | 21 ++++++------
 .../permission/CrmPermissionServiceImpl.java  |  9 +++++-
 .../permission/bo/CrmPermissionPageReqBO.java | 32 +++++++++++++++++++
 14 files changed, 98 insertions(+), 26 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionPageReqBO.java

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index eb83fa743..c1ccaf9b7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -71,8 +71,7 @@ public class CrmBusinessController {
     @Operation(summary = "获得商机分页")
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
     public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPage(@Valid CrmBusinessPageReqVO pageVO) {
-        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO);
-
+        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO, getLoginUserId());
         return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index 3c27c0ce3..9e23c793a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.business;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
@@ -37,4 +38,6 @@ public interface CrmBusinessConvert {
     })
     CrmTransferPermissionReqBO convert(CrmBusinessTransferReqVO reqVO, Long userId);
 
+    CrmPermissionPageReqBO convert(CrmBusinessPageReqVO pageReqVO);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
index 2601b5d09..bcd12e213 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
@@ -23,6 +23,12 @@ import lombok.*;
 @AllArgsConstructor
 public class CrmPermissionDO extends BaseDO {
 
+    /**
+     * 当数据变为公海数据时,也就是数据团队成员中没有负责人的时候,将原本的负责人 userId 设置为 POOL_USER_ID 方便查询公海数据。
+     * 也就是说每条数据到最后都有一个负责人,如果有人领取则 userId 为领取人
+     */
+    public static final Long POOL_USER_ID = 0L;
+
     /**
      * ID
      */
@@ -38,12 +44,12 @@ public class CrmPermissionDO extends BaseDO {
     private Long bizId;
     /**
      * 团队成员,关联 AdminUser#id
+     * 如果为公海数据的话会干掉此数据的负责人后设置为 {@link #POOL_USER_ID},领取人则上位负责人
+     * 例:客户放入公海后会干掉团队成员中的负责人,而其他团队成员则不受影响
      */
     private Long userId;
     /**
      * 权限级别,关联 {@link CrmPermissionLevelEnum}
-     * 如果为公海数据的话会干掉此数据的负责人,领取人则上位负责人
-     * 例:客户放入公海后会干掉团队成员中的负责人,而其他团队成员则不受影响
      */
     private Integer permissionLevel;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
index 5df94c0fa..0ae83c236 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.business;
 
-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.business.vo.CrmBusinessExportReqVO;
@@ -8,6 +7,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.CrmBusinessPageR
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -18,8 +18,9 @@ import java.util.List;
 @Mapper
 public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
 
-    default PageResult<CrmBusinessDO> selectPage(CrmBusinessPageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<CrmBusinessDO>()
+    default List<CrmBusinessDO> selectList(CrmBusinessPageReqVO reqVO, Collection<Long> ids) {
+        return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
+                .in(CrmBusinessDO::getId, ids)
                 .likeIfPresent(CrmBusinessDO::getName, reqVO.getName())
                 .orderByDesc(CrmBusinessDO::getId));
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index c2db6acbf..45c28fb4c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.permission;
 
+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.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.List;
@@ -57,4 +59,10 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 .eq(CrmPermissionDO::getUserId, userId));
     }
 
+    default PageResult<CrmPermissionDO> selectPage(CrmPermissionPageReqBO pageReqBO) {
+        return selectPage(pageReqBO, new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getBizType, pageReqBO.getBizType())
+                .eq(CrmPermissionDO::getUserId, pageReqBO.getUserId())); // 只要是团队成员都有读取的权限
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index ee921d2c7..bb1d25b59 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -81,7 +81,8 @@ public class CrmPermissionAspect {
             }
             if (isRead(permissionLevel)) { // 读权限
                 // 如果没有数据权限或没有负责人则表示此记录为公海数据所有人都有只读权限可以领取成为负责人(团队成员领取的)
-                if (CollUtil.isEmpty(bizPermissions) || !CollUtil.anyMatch(bizPermissions, item -> isOwner(item.getPermissionLevel()))) {
+                if (CollUtil.isEmpty(bizPermissions) || CollUtil.anyMatch(bizPermissions,
+                        item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID))) { // 详见 CrmPermissionDO.POOL_USER_ID 注释
                     return;
                 }
                 if (isRead(userPermission.getPermissionLevel())) { // 校验当前用户是否有读权限
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index aeee4b426..ec5dbe1d4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -58,9 +58,10 @@ public interface CrmBusinessService {
      * 获得商机分页
      *
      * @param pageReqVO 分页查询
+     * @param userId    用户编号
      * @return 商机分页
      */
-    PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO);
+    PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO, Long userId);
 
     /**
      * 获得商机列表, 用于 Excel 导出
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index b3e1b9790..c4dc4441c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
@@ -19,8 +20,10 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import java.util.Collection;
 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;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
 
 /**
@@ -98,8 +101,18 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     }
 
     @Override
-    public PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO) {
-        return businessMapper.selectPage(pageReqVO);
+    public PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO, Long userId) {
+        // 1. 获取当前用户能看的分页数据
+        PageResult<CrmPermissionDO> permissionPage = crmPermissionService.getPermissionPage(
+                CrmBusinessConvert.INSTANCE.convert(pageReqVO).setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()).setUserId(userId));
+        Set<Long> ids = convertSet(permissionPage.getList(), CrmPermissionDO::getBizId);
+        if (CollUtil.isEmpty(ids)) { // 没得说明没有什么给他看的
+            return PageResult.empty();
+        }
+
+        // 2. 获取商机分页数据
+        List<CrmBusinessDO> businessList = businessMapper.selectList(pageReqVO, ids);
+        return new PageResult<>(businessList, (long) businessList.size());
     }
 
     @Override
@@ -114,7 +127,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         validateBusinessExists(reqVO.getId());
 
         // 2. 数据权限转移
-        crmPermissionService.transferCrmPermission(
+        crmPermissionService.transferPermission(
                 CrmBusinessConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()));
 
         // 3. TODO 记录转移日志
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index 550829fbe..dafebb598 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -115,7 +115,7 @@ public class ContactServiceImpl implements ContactService {
         validateContactExists(reqVO.getId());
 
         // 2. 数据权限转移
-        crmPermissionService.transferCrmPermission(
+        crmPermissionService.transferPermission(
                 ContactConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTACTS.getType()));
 
         // 3. TODO 记录转移日志
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index c4b144462..523470cd8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -113,7 +113,7 @@ public class ContractServiceImpl implements ContractService {
         validateContractExists(reqVO.getId());
 
         // 2. 数据权限转移
-        crmPermissionService.transferCrmPermission(
+        crmPermissionService.transferPermission(
                 ContractConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()));
 
         // 3. TODO 记录转移日志
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 87d23a25f..e77ceb042 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -136,7 +136,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         validateCustomer(reqVO.getId());
 
         // 2. 数据权限转移
-        crmPermissionService.transferCrmPermission(
+        crmPermissionService.transferPermission(
                 CrmCustomerConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType()));
 
         // 3. TODO 记录转移日志
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index 5e2daef08..59d160773 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.crm.service.permission;
 
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 
@@ -58,20 +60,19 @@ public interface CrmPermissionService {
      */
     List<CrmPermissionDO> getPermissionByBizTypeAndBizId(Integer bizType, Long bizId);
 
-    /**
-     * 获取数据权限列表,通过 数据类型 x 用户编号
-     *
-     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
-     * @param userId  用户编号
-     * @return Crm 数据权限列表
-     */
-    List<CrmPermissionDO> getPermissionByBizTypeAndUserId(Integer bizType, Long userId);
-
     /**
      * 数据权限转移
      *
      * @param crmTransferPermissionReqBO 数据权限转移请求
      */
-    void transferCrmPermission(@Valid CrmTransferPermissionReqBO crmTransferPermissionReqBO);
+    void transferPermission(@Valid CrmTransferPermissionReqBO crmTransferPermissionReqBO);
+
+    /**
+     * 获取数据权限分页数据
+     *
+     * @param pageReqBO 分页请求
+     * @return 数据权限分页数据
+     */
+    PageResult<CrmPermissionDO> getPermissionPage(CrmPermissionPageReqBO pageReqBO);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index 8d4047741..25fa5c849 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -2,12 +2,14 @@ package cn.iocoder.yudao.module.crm.service.permission;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -86,7 +88,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
 
 
     @Override
-    public void transferCrmPermission(CrmTransferPermissionReqBO transferReqBO) {
+    public void transferPermission(CrmTransferPermissionReqBO transferReqBO) {
         // 1. 校验数据权限-是否是负责人,只有负责人才可以转移
         CrmPermissionDO oldPermission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(transferReqBO.getBizType(),
                 transferReqBO.getBizId(), transferReqBO.getUserId());
@@ -131,4 +133,9 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         crmPermissionMapper.deleteById(oldPermission.getId()); // 移除
     }
 
+    @Override
+    public PageResult<CrmPermissionDO> getPermissionPage(CrmPermissionPageReqBO pageReqBO) {
+        return crmPermissionMapper.selectPage(pageReqBO);
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionPageReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionPageReqBO.java
new file mode 100644
index 000000000..736171c1e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionPageReqBO.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.crm.service.permission.bo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 数据权限分页 Request BO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmPermissionPageReqBO extends PageParam {
+
+    /**
+     * 当前登录用户编号
+     */
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    /**
+     * Crm 类型
+     */
+    @NotNull(message = "Crm 类型不能为空")
+    @InEnum(CrmBizTypeEnum.class)
+    private Integer bizType;
+
+}

From 23cfbfe4912fd02b06283615dd343cd059a54ca5 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Wed, 1 Nov 2023 16:24:36 +0800
Subject: [PATCH 056/101] =?UTF-8?q?CRM-=E6=95=B0=E6=8D=AE=E6=9D=83?=
 =?UTF-8?q?=E9=99=90=EF=BC=9A=E5=AE=8C=E5=96=84=E6=95=B0=E6=8D=AE=E5=9B=A2?=
 =?UTF-8?q?=E9=98=9F=E6=93=8D=E4=BD=9C-=E6=B7=BB=E5=8A=A0=E3=80=81?=
 =?UTF-8?q?=E7=BC=96=E8=BE=91=E3=80=81=E7=A7=BB=E9=99=A4=E5=9B=A2=E9=98=9F?=
 =?UTF-8?q?=E6=88=90=E5=91=98=EF=BC=8C=E9=80=80=E5=87=BA=E5=9B=A2=E9=98=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |   3 +-
 .../admin/business/CrmBusinessController.java |   1 -
 .../permission/CrmPermissionController.java   | 146 ++++++++++++++++++
 .../permission/vo/CrmPermissionBaseVO.java    |  38 +++++
 .../vo/CrmPermissionCreateReqVO.java          |  14 ++
 .../permission/vo/CrmPermissionRespVO.java    |  27 ++++
 .../vo/CrmPermissionUpdateReqVO.java          |  17 ++
 .../permission/CrmPermissionConvert.java      |  28 ++++
 .../mysql/permission/CrmPermissionMapper.java |  13 --
 .../service/CrmPermissionValidateService.java |  20 +++
 .../enums/CrmPermissionLevelEnum.java         |   9 ++
 .../permission/CrmPermissionServiceImpl.java  |  23 +--
 12 files changed, 312 insertions(+), 27 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/service/CrmPermissionValidateService.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 5aeb6ddf7..9e1de7bad 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -38,7 +38,6 @@ public interface ErrorCodeConstants {
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
     ErrorCode CRM_PERMISSION_DENIED = new ErrorCode(1_020_007_001, "{}操作失败,原因:没有权限");
     ErrorCode CRM_PERMISSION_MODEL_NOT_EXISTS = new ErrorCode(1_020_007_002, "{}不存在");
-    ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS = new ErrorCode(1_020_007_003, "{}操作失败,原因:负责人不存在");
-    ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_007_004, "{}操作失败,原因:转移对象已经是该负责人");
+    ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_007_003, "{}操作失败,原因:转移对象已经是该负责人");
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index c1ccaf9b7..cebfc94c2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -95,5 +95,4 @@ public class CrmBusinessController {
         return success(true);
     }
 
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
new file mode 100644
index 000000000..4e038c69d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.crm.controller.admin.permission;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.framework.core.service.CrmPermissionValidateService;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_DENIED;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_MODEL_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum.getNameByType;
+import static cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum.isOwner;
+
+@Tag(name = "管理后台 - CRM 数据权限(数据团队成员操作)")
+@RestController
+@RequestMapping("/crm/permission")
+@Validated
+public class CrmPermissionController {
+
+    @Resource
+    private CrmPermissionService crmPermissionService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private List<CrmPermissionValidateService> permissionValidateServices;
+
+    private void validatePermission(Integer bizType, Long bizId) {
+        // 1. TODO 校验是否为超级管理员
+        // 2. 防御一手,如果是超级管理员不校验权限还是得校验一下数据是否存在
+        permissionValidateServices.forEach(item -> {
+            if (!item.validateBizIdExists(bizType, bizId)) {
+                throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, getNameByType(bizType));
+            }
+        });
+        // 3. 校验数据权限 (如果存在则表示 bizId 也存在)
+        CrmPermissionDO permission = crmPermissionService.getPermissionByBizTypeAndBizIdAndUserId(
+                bizType, bizId, getLoginUserId());
+        if (isOwner(permission.getPermissionLevel())) { // 只有负责人才可以操作团队成员
+            return;
+        }
+        throw exception(CRM_PERMISSION_DENIED, getNameByType(bizType));
+    }
+
+    @PutMapping("/add")
+    @Operation(summary = "添加团队成员")
+    @PreAuthorize("@ss.hasPermission('crm:permission:create')")
+    public CommonResult<Boolean> addPermission(@Valid @RequestBody CrmPermissionCreateReqVO reqVO) {
+        // 1. 前置校验
+        validatePermission(reqVO.getBizType(), reqVO.getBizId());
+
+        // 2. 加入成员
+        crmPermissionService.createPermission(CrmPermissionConvert.INSTANCE.convert(reqVO));
+        return success(true);
+    }
+
+
+    @PutMapping("/update")
+    @Operation(summary = "编辑团队成员")
+    @PreAuthorize("@ss.hasPermission('crm:permission:update')")
+    public CommonResult<Boolean> updatePermission(@Valid @RequestBody CrmPermissionUpdateReqVO updateReqVO) {
+        // 1. 前置校验
+        validatePermission(updateReqVO.getBizType(), updateReqVO.getBizId());
+
+        // 2. 编辑团队成员
+        crmPermissionService.updatePermission(CrmPermissionConvert.INSTANCE.convert(updateReqVO));
+        return success(true);
+    }
+
+    @GetMapping("/delete")
+    @Operation(summary = "移除团队成员")
+    @Parameter(name = "id", description = "团队成员编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
+    public CommonResult<Boolean> deletePermission(@RequestParam("bizType") Integer bizType,
+                                                  @RequestParam("bizId") Long bizId,
+                                                  @RequestParam("id") Long id) {
+        // 1. 前置校验
+        validatePermission(bizType, bizId);
+
+        // 2. 移除团队成员
+        crmPermissionService.deletePermission(id);
+        return success(true);
+    }
+
+    @GetMapping("/quit")
+    @Operation(summary = "退出团队")
+    @Parameters({
+            @Parameter(name = "bizType", description = "CRM 类型", required = true, example = "2"),
+            @Parameter(name = "bizId", description = "CRM 类型数据编号", required = true, example = "1024")
+    })
+    @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
+    public CommonResult<Boolean> quitPermission(@RequestParam("bizType") Integer bizType,
+                                                @RequestParam("bizId") Long bizId) {
+        CrmPermissionDO permission = crmPermissionService.getPermissionByBizTypeAndBizIdAndUserId(
+                bizType, bizId, getLoginUserId());
+        if (permission == null) { // 没有就不是团队成员
+            return success(false);
+        }
+        crmPermissionService.deletePermission(permission.getId());
+        return success(true);
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获取团队成员")
+    @Parameters({
+            @Parameter(name = "bizType", description = "CRM 类型", required = true, example = "2"),
+            @Parameter(name = "bizId", description = "CRM 类型数据编号", required = true, example = "1024")
+    })
+    @PreAuthorize("@ss.hasPermission('crm:permission:query')")
+    public CommonResult<List<CrmPermissionRespVO>> getPermissionList(@RequestParam("bizType") Integer bizType,
+                                                                     @RequestParam("bizId") Long bizId) {
+        List<CrmPermissionDO> permission = crmPermissionService.getPermissionByBizTypeAndBizId(bizType, bizId);
+        if (CollUtil.isEmpty(permission)) {
+            return success(Collections.emptyList());
+        }
+        permission.removeIf(item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID)); // 排除
+
+        // 拼接数据
+        List<AdminUserRespDTO> userList = adminUserApi.getUserList(convertSet(permission, CrmPermissionDO::getUserId));
+        return success(CrmPermissionConvert.INSTANCE.convert(permission, userList));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
new file mode 100644
index 000000000..9fb1db759
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.controller.admin.permission.vo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 数据权限(团队成员) Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CrmPermissionBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @Schema(description = "Crm 类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @InEnum(CrmBizTypeEnum.class)
+    @NotNull(message = "Crm 类型不能为空")
+    private Integer bizType;
+
+    @Schema(description = "Crm 类型数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "Crm 类型数据编号不能为空")
+    private Long bizId;
+
+    @Schema(description = "权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @InEnum(CrmPermissionLevelEnum.class)
+    @NotNull(message = "权限级别不能为空")
+    private Integer permissionLevel;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionCreateReqVO.java
new file mode 100644
index 000000000..99793389b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.permission.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 数据权限创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmPermissionCreateReqVO extends CrmPermissionBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
new file mode 100644
index 000000000..0ded2f735
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.crm.controller.admin.permission.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Set;
+
+@Schema(description = "管理后台 - CRM 数据权限(团队成员) Response VO")
+@Data
+public class CrmPermissionRespVO extends CrmPermissionBaseVO {
+
+    @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    private Long id;
+
+    @Schema(description = "团队级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "负责人")
+    private String permissionLevelName;
+
+    @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long deptId;
+
+    @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    private String nickname;
+
+    @Schema(description = "岗位编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]")
+    private Set<Long> postIds;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
new file mode 100644
index 000000000..f0a11cf39
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.crm.controller.admin.permission.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 数据权限更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmPermissionUpdateReqVO extends CrmPermissionBaseVO {
+
+    @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
index 22e49b61f..9fa7e5fad 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
@@ -1,11 +1,22 @@
 package cn.iocoder.yudao.module.crm.convert.permission;
 
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
+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;
+
+import static cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum.getNameByLevel;
+
 /**
  * Crm 数据权限 Convert
  *
@@ -20,4 +31,21 @@ public interface CrmPermissionConvert {
 
     CrmPermissionDO convert(CrmPermissionUpdateReqBO updateBO);
 
+    CrmPermissionCreateReqBO convert(CrmPermissionCreateReqVO reqVO);
+
+    CrmPermissionUpdateReqBO convert(CrmPermissionUpdateReqVO updateReqVO);
+
+    List<CrmPermissionRespVO> convert(List<CrmPermissionDO> permission);
+
+    default List<CrmPermissionRespVO> convert(List<CrmPermissionDO> permission, List<AdminUserRespDTO> userList) {
+        Map<Long, AdminUserRespDTO> userMap = CollectionUtils.convertMap(userList, AdminUserRespDTO::getId);
+        return CollectionUtils.convertList(convert(permission), item -> {
+            MapUtils.findAndThen(userMap, item.getId(), user -> {
+                item.setNickname(user.getNickname()).setDeptId(user.getDeptId()).setPostIds(user.getPostIds())
+                        .setPermissionLevelName(getNameByLevel(item.getPermissionLevel()));
+            });
+            return item;
+        });
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index 45c28fb4c..218fdad19 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -46,19 +46,6 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 .eq(CrmPermissionDO::getBizId, bizId));
     }
 
-    /**
-     * 获取数据权限列表通过 数据类型 x 用户编号。如果
-     *
-     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
-     * @param userId
-     * @return Crm 数据权限列表
-     */
-    default List<CrmPermissionDO> selectByBizTypeAndUserId(Integer bizType, Long userId) {
-        return selectList(new LambdaQueryWrapperX<CrmPermissionDO>()
-                .eq(CrmPermissionDO::getBizType, bizType)
-                .eq(CrmPermissionDO::getUserId, userId));
-    }
-
     default PageResult<CrmPermissionDO> selectPage(CrmPermissionPageReqBO pageReqBO) {
         return selectPage(pageReqBO, new LambdaQueryWrapperX<CrmPermissionDO>()
                 .eq(CrmPermissionDO::getBizType, pageReqBO.getBizType())
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/service/CrmPermissionValidateService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/service/CrmPermissionValidateService.java
new file mode 100644
index 000000000..7112d95b5
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/service/CrmPermissionValidateService.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.framework.core.service;
+
+/**
+ * 校验数据是否存在 service 接口
+ * TODO 需要使用团队成员相关操作的业务接口都需要继承此接口
+ *
+ * @author HUIHUI
+ */
+public interface CrmPermissionValidateService {
+
+    /**
+     * 校验数据是否存在
+     *
+     * @param bizType CRM 类型
+     * @param bizId   数据编号
+     * @return 是/否
+     */
+    boolean validateBizIdExists(Integer bizType, Long bizId);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
index 9df93f253..882719c79 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
@@ -47,4 +47,13 @@ public enum CrmPermissionLevelEnum implements IntArrayValuable {
         return ObjUtil.equal(WRITE.level, level);
     }
 
+    public static String getNameByLevel(Integer level) {
+        for (CrmPermissionLevelEnum levelEnum : CrmPermissionLevelEnum.values()) {
+            if (ObjUtil.equal(levelEnum.level, level)) {
+                return levelEnum.name;
+            }
+        }
+        return "";
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index 25fa5c849..c2e08fc21 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -13,12 +13,12 @@ import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
-import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
+import java.util.Collections;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -43,6 +43,10 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createPermission(CrmPermissionCreateReqBO createBO) {
+        // 1. 校验用户是否存在
+        adminUserApi.validateUserList(Collections.singletonList(createBO.getUserId()));
+
+        // 2. 创建
         CrmPermissionDO permission = CrmPermissionConvert.INSTANCE.convert(createBO);
         crmPermissionMapper.insert(permission);
         return permission.getId();
@@ -51,7 +55,11 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updatePermission(CrmPermissionUpdateReqBO updateBO) {
+        // 1. 校验用户是否存在
+        adminUserApi.validateUserList(Collections.singletonList(updateBO.getUserId()));
+        // 2. 校验存在
         validateCrmPermissionExists(updateBO.getId());
+
         // 更新操作
         CrmPermissionDO updateDO = CrmPermissionConvert.INSTANCE.convert(updateBO);
         crmPermissionMapper.updateById(updateDO);
@@ -60,7 +68,9 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void deletePermission(Long id) {
+        // 校验存在
         validateCrmPermissionExists(id);
+
         // 删除
         crmPermissionMapper.deleteById(id);
     }
@@ -75,11 +85,6 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         return crmPermissionMapper.selectByBizTypeAndBizId(bizType, bizId);
     }
 
-    @Override
-    public List<CrmPermissionDO> getPermissionByBizTypeAndUserId(Integer bizType, Long userId) {
-        return crmPermissionMapper.selectByBizTypeAndUserId(bizType, userId);
-    }
-
     private void validateCrmPermissionExists(Long id) {
         if (crmPermissionMapper.selectById(id) == null) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
@@ -103,11 +108,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
             throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, crmName);
         }
         // 2.1 校验新负责人是否存在
-        AdminUserRespDTO user = adminUserApi.getUser(transferReqBO.getNewOwnerUserId());
-        if (user == null) {
-            throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_NOT_EXISTS, crmName);
-        }
-
+        adminUserApi.validateUserList(Collections.singletonList(transferReqBO.getNewOwnerUserId()));
         // 3. 权限转移
         List<CrmPermissionDO> permissions = crmPermissionMapper.selectByBizTypeAndBizId(
                 transferReqBO.getBizType(), transferReqBO.getBizId()); // 获取所有团队成员

From 68c9d563bc5a2187064bf2d4a898b80fa92907c9 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Wed, 1 Nov 2023 16:39:23 +0800
Subject: [PATCH 057/101] =?UTF-8?q?CRM-=E5=95=86=E6=9C=BA=EF=BC=9A?=
 =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=8E=B7=E5=BE=97=E5=95=86=E6=9C=BA=E5=85=AC?=
 =?UTF-8?q?=E6=B5=B7=E5=88=86=E9=A1=B5=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/business/CrmBusinessController.java         |  9 +++++++++
 .../crm/service/business/CrmBusinessService.java      |  3 ++-
 .../crm/service/business/CrmBusinessServiceImpl.java  | 11 +++++++++++
 3 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index cebfc94c2..49c99991e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -75,6 +76,14 @@ public class CrmBusinessController {
         return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult));
     }
 
+    @GetMapping("/pool-page")
+    @Operation(summary = "获得商机公海分页")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPoolPage(@Valid CrmBusinessPageReqVO pageVO) {
+        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO, CrmPermissionDO.POOL_USER_ID);
+        return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult));
+    }
+
     @GetMapping("/export-excel")
     @Operation(summary = "导出商机 Excel")
     @PreAuthorize("@ss.hasPermission('crm:business:export')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index ec5dbe1d4..c20aadcf5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.service.business;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.framework.core.service.CrmPermissionValidateService;
 
 import javax.validation.Valid;
 import java.util.Collection;
@@ -13,7 +14,7 @@ import java.util.List;
  *
  * @author ljlleo
  */
-public interface CrmBusinessService {
+public interface CrmBusinessService extends CrmPermissionValidateService {
 
     /**
      * 创建商机
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index c4dc4441c..20b844ced 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.service.business;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
@@ -133,4 +134,14 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         // 3. TODO 记录转移日志
     }
 
+    @Override
+    public boolean validateBizIdExists(Integer bizType, Long bizId) {
+        // 1. 校验模块类型
+        if (!ObjUtil.equal(CrmBizTypeEnum.CRM_BUSINESS.getType(), bizId)) {
+            return false;
+        }
+        // 2. 校验是否存在
+        return businessMapper.selectById(bizId) != null;
+    }
+
 }

From 4092f298ed15b4e3256c698068c41d50d6f2333f Mon Sep 17 00:00:00 2001
From: "zhijiantianya@gmail.com" <zhijiantianya@gmail.com>
Date: Fri, 3 Nov 2023 18:53:40 +0800
Subject: [PATCH 058/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E6=9D=83?=
 =?UTF-8?q?=E9=99=90=E6=A8=A1=E5=9D=97=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../business/vo/CrmBusinessTransferReqVO.java |  1 +
 .../contact/vo/CrmContactTransferReqVO.java   |  2 +-
 .../contract/vo/CrmContractTransferReqVO.java |  1 +
 .../customer/vo/CrmCustomerTransferReqVO.java |  1 +
 .../permission/CrmPermissionController.java   | 10 ++++++++-
 .../permission/vo/CrmPermissionBaseVO.java    |  1 +
 .../permission/vo/CrmPermissionRespVO.java    |  2 ++
 .../vo/CrmPermissionUpdateReqVO.java          |  3 +++
 .../permission/CrmPermissionDO.java           | 22 +++++++++++++++----
 .../mysql/permission/CrmPermissionMapper.java |  2 ++
 .../core/annotations/CrmPermission.java       |  2 ++
 .../core/aop/CrmPermissionAspect.java         | 20 ++++++++++++-----
 .../crm/framework/enums/CrmBizTypeEnum.java   |  1 +
 .../enums/CrmPermissionLevelEnum.java         |  2 ++
 .../permission/CrmPermissionServiceImpl.java  |  3 ++-
 .../bo/CrmPermissionCreateReqBO.java          |  3 +--
 .../bo/CrmPermissionUpdateReqBO.java          |  6 ++---
 .../bo/CrmTransferPermissionReqBO.java        |  3 ++-
 18 files changed, 67 insertions(+), 18 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
index aa85404a7..fc1ab74c6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
@@ -21,6 +21,7 @@ public class CrmBusinessTransferReqVO {
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
+    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
      * 老负责人是否加入团队,是/否
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
index fa8940ea3..6ca37d138 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
@@ -21,13 +21,13 @@ public class CrmContactTransferReqVO {
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
+    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
      * 老负责人是否加入团队,是/否
      */
     @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
     @NotNull(message = "老负责人是否加入团队不能为空")
     private Boolean joinTeam;
-
     /**
      * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
      * 关联 {@link CrmPermissionLevelEnum}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
index cf3e0effe..241693cd3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
@@ -21,6 +21,7 @@ public class CrmContractTransferReqVO {
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
+    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
      * 老负责人是否加入团队,是/否
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
index 20f895197..990eaed3d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
@@ -21,6 +21,7 @@ public class CrmCustomerTransferReqVO {
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
+    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
      * 老负责人是否加入团队,是/否
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
index 4e038c69d..8402b4f49 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
@@ -48,9 +48,11 @@ public class CrmPermissionController {
     @Resource
     private List<CrmPermissionValidateService> permissionValidateServices;
 
+    // TODO @puhui999:这个能不能使用 CrmPermission 注解替代?
     private void validatePermission(Integer bizType, Long bizId) {
         // 1. TODO 校验是否为超级管理员
         // 2. 防御一手,如果是超级管理员不校验权限还是得校验一下数据是否存在
+        // TODO @puhui999:是不是不用校验每个业务方的数据是否存在;其实不是很关键哈;简单一点~ 说白了,负责人只要在,它的数据就是存在~
         permissionValidateServices.forEach(item -> {
             if (!item.validateBizIdExists(bizType, bizId)) {
                 throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, getNameByType(bizType));
@@ -90,9 +92,11 @@ public class CrmPermissionController {
         return success(true);
     }
 
+    // TODO @puhui999:deletemapping
     @GetMapping("/delete")
     @Operation(summary = "移除团队成员")
     @Parameter(name = "id", description = "团队成员编号", required = true)
+    // TODO @puhui999:是不是 id 参数就够了?
     @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
     public CommonResult<Boolean> deletePermission(@RequestParam("bizType") Integer bizType,
                                                   @RequestParam("bizId") Long bizId,
@@ -105,6 +109,8 @@ public class CrmPermissionController {
         return success(true);
     }
 
+    // TODO @puhui999:这个是哪个地方使用到哈?
+    // TODO @puhui999:是不是 deletemapping 呀;
     @GetMapping("/quit")
     @Operation(summary = "退出团队")
     @Parameters({
@@ -114,9 +120,10 @@ public class CrmPermissionController {
     @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
     public CommonResult<Boolean> quitPermission(@RequestParam("bizType") Integer bizType,
                                                 @RequestParam("bizId") Long bizId) {
+        // 没有就不是团队成员
         CrmPermissionDO permission = crmPermissionService.getPermissionByBizTypeAndBizIdAndUserId(
                 bizType, bizId, getLoginUserId());
-        if (permission == null) { // 没有就不是团队成员
+        if (permission == null) {
             return success(false);
         }
         crmPermissionService.deletePermission(permission.getId());
@@ -136,6 +143,7 @@ public class CrmPermissionController {
         if (CollUtil.isEmpty(permission)) {
             return success(Collections.emptyList());
         }
+        // TODO @puhui999:池子的逻辑;
         permission.removeIf(item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID)); // 排除
 
         // 拼接数据
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
index 9fb1db759..98ac5831d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
@@ -30,6 +30,7 @@ public class CrmPermissionBaseVO {
     @NotNull(message = "Crm 类型数据编号不能为空")
     private Long bizId;
 
+    // TODO @puhui999:level;
     @Schema(description = "权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     @InEnum(CrmPermissionLevelEnum.class)
     @NotNull(message = "权限级别不能为空")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
index 0ded2f735..9b7df8c16 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
@@ -12,9 +12,11 @@ public class CrmPermissionRespVO extends CrmPermissionBaseVO {
     @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     private Long id;
 
+    // TODO @puhui999:搞到字典里;
     @Schema(description = "团队级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "负责人")
     private String permissionLevelName;
 
+    // TODO @puhui999:deptId、postIds 是不是要提供中文名哈;
     @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Long deptId;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
index f0a11cf39..328a774f1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
@@ -12,6 +12,9 @@ import lombok.ToString;
 public class CrmPermissionUpdateReqVO extends CrmPermissionBaseVO {
 
     @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    // TODO @puhui999:非空判断;
     private Long id;
 
+    // TODO @puhui999:是不是只更新 permission???
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
index bcd12e213..263a8fd7a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
@@ -23,6 +23,7 @@ import lombok.*;
 @AllArgsConstructor
 public class CrmPermissionDO extends BaseDO {
 
+    // TODO puhui999:是不是公海的数据,就不插入了;
     /**
      * 当数据变为公海数据时,也就是数据团队成员中没有负责人的时候,将原本的负责人 userId 设置为 POOL_USER_ID 方便查询公海数据。
      * 也就是说每条数据到最后都有一个负责人,如果有人领取则 userId 为领取人
@@ -34,22 +35,35 @@ public class CrmPermissionDO extends BaseDO {
      */
     @TableId
     private Long id;
+
     /**
-     * 数据类型,关联 {@link CrmBizTypeEnum}
+     * 数据类型
+     *
+     * 枚举 {@link CrmBizTypeEnum}
      */
     private Integer bizType;
     /**
-     * 数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * 数据编号
+     *
+     * 关联 {@link CrmBizTypeEnum} 对应模块 DO 的 id 字段
      */
     private Long bizId;
+
     /**
-     * 团队成员,关联 AdminUser#id
+     * 团队成员
+     *
+     * 关联 AdminUser 的 id 字段
+     *
      * 如果为公海数据的话会干掉此数据的负责人后设置为 {@link #POOL_USER_ID},领取人则上位负责人
      * 例:客户放入公海后会干掉团队成员中的负责人,而其他团队成员则不受影响
      */
     private Long userId;
+
+    // TODO @puhui999:是不是搞成 level 字段;简洁一点,主要表明已经 perssmion 实体里了;
     /**
-     * 权限级别,关联 {@link CrmPermissionLevelEnum}
+     * 权限级别
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
      */
     private Integer permissionLevel;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index 218fdad19..3d006d0f8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -18,6 +18,7 @@ import java.util.List;
 @Mapper
 public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
 
+    // TODO @puhui999:是不是不用谢这个注释;因为方法名,可以自解释;
     /**
      * 获取用户数据权限通过 数据类型 x 某个数据 x 用户编号
      *
@@ -33,6 +34,7 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 .eq(CrmPermissionDO::getUserId, userId));
     }
 
+    // TODO @puhui999:是不是不用谢这个注释;因为方法名,可以自解释;
     /**
      * 获取数据权限列表通过 数据类型 x 某个数据
      *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
index 082b37d0e..d63ecf3ee 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
@@ -26,12 +26,14 @@ public @interface CrmPermission {
      */
     CrmBizTypeEnum bizType();
 
+    // TODO @puhui999:id,通过 spring el 表达式获取;
     /**
      * 数据编号获取来源类,确保数据 id 编号在此类中,不能在父类中。
      * 例:如果在 baseVO 中需要把 id 弄到 updateVO 中。
      */
     Class<?>[] getIdFor() default {};
 
+    // TODO @puhui999:是不是搞成 level 字段;简洁一点,主要表明已经 perssmion 实体里了;
     /**
      * 操作类型
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index bb1d25b59..8a60b6d45 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -43,6 +43,7 @@ public class CrmPermissionAspect {
         return WebFrameworkUtils.getLoginUserId();
     }
 
+    // TODO @puhui999:id,通过 spring el 表达式获取;
     private Long getBizId(JoinPoint joinPoint, CrmPermission crmPermission) throws NoSuchFieldException, IllegalAccessException {
         Object[] args = joinPoint.getArgs();
         for (Object arg : args) {
@@ -61,6 +62,7 @@ public class CrmPermissionAspect {
         return null;
     }
 
+    // TODO @puhui999:一般核心的方法,放到最前面,private 放后面。主要是,主次要分出来哈;
     @Before("@annotation(crmPermission)")
     public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
         try {
@@ -75,12 +77,17 @@ public class CrmPermissionAspect {
 
             // 1. 获取数据权限
             List<CrmPermissionDO> bizPermissions = crmPermissionService.getPermissionByBizTypeAndBizId(bizType, bizId);
+            // TODO puhui999:这种情况下,最好是 CrmPermissionLevelEnum.isOwner
+            // 2.1 情况一:如果自己是负责人,则默认有所有权限
+            // TODO @puhui999:会不会存在空指针的问题?
             CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, item -> ObjUtil.equal(item.getUserId(), getUserId()));
-            if (isOwner(userPermission.getPermissionLevel())) { // 校验自己是否是负责人
+            if (isOwner(userPermission.getPermissionLevel())) {
                 return;
             }
-            if (isRead(permissionLevel)) { // 读权限
+            // 2.2 情况二:校验自己是否有读权限
+            if (isRead(permissionLevel)) {
                 // 如果没有数据权限或没有负责人则表示此记录为公海数据所有人都有只读权限可以领取成为负责人(团队成员领取的)
+                // TODO @puhui999:89 到 92 这块的逻辑,感觉可以不用 @CrmPermission,公海那自己 check 即可;
                 if (CollUtil.isEmpty(bizPermissions) || CollUtil.anyMatch(bizPermissions,
                         item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID))) { // 详见 CrmPermissionDO.POOL_USER_ID 注释
                     return;
@@ -88,20 +95,23 @@ public class CrmPermissionAspect {
                 if (isRead(userPermission.getPermissionLevel())) { // 校验当前用户是否有读权限
                     return;
                 }
-                //如果查询数据的话拥有写权限的也能查询
+                // 如果查询数据的话拥有写权限的也能查询
                 if (isWrite(userPermission.getPermissionLevel())) { // 校验当前用户是否有写权限
                     return;
                 }
             }
-            if (isWrite(permissionLevel)) { // 写权限
+            // 2.3 情况三:校验自己是否有写权限
+            if (isWrite(permissionLevel)) {
                 if (isWrite(userPermission.getPermissionLevel())) { // 校验当前用户是否有写权限
                     return;
                 }
             }
 
-            // 2. 没通过结束,报错 {}操作失败,原因:没有权限
+            // 3. 没通过结束,报错 {} 操作失败,原因:没有权限
+            // TODO @puhui999:这里打个 info 日志,方便后续排查问题、审计;
             throw exception(CRM_PERMISSION_DENIED, crmPermission.bizType().getName());
         } catch (Exception ex) {
+            // TODO @puhui999:不用 catch 掉,就是系统异常;
             log.error("[doBefore][crmPermission({}) 数据校验错误]", toJsonString(crmPermission), ex);
             // TODO 报错抛个什么异常好呢
             throw exception(CRM_PERMISSION_DENIED, crmPermission.bizType().getName());
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
index 08766f63e..c805615cd 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
@@ -33,6 +33,7 @@ public enum CrmBizTypeEnum implements IntArrayValuable {
     private final String name;
 
     public static String getNameByType(Integer type) {
+        // TODO @puhui999:可以 findone,更简洁;另外,不存在返回 null 即可啦;
         for (CrmBizTypeEnum crmBizTypeEnum : CrmBizTypeEnum.values()) {
             if (ObjUtil.equal(crmBizTypeEnum.type, type)) {
                 return crmBizTypeEnum.name;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
index 882719c79..6eeddc78b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
@@ -19,6 +19,7 @@ public enum CrmPermissionLevelEnum implements IntArrayValuable {
     OWNER(1, "负责人"),
     READ(2, "读"),
     WRITE(3, "写");
+
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmPermissionLevelEnum::getLevel).toArray();
 
     /**
@@ -48,6 +49,7 @@ public enum CrmPermissionLevelEnum implements IntArrayValuable {
     }
 
     public static String getNameByLevel(Integer level) {
+        // TODO @puhui999:可以 findone,更简洁;另外,不存在返回 null 即可啦;
         for (CrmPermissionLevelEnum levelEnum : CrmPermissionLevelEnum.values()) {
             if (ObjUtil.equal(levelEnum.level, level)) {
                 return levelEnum.name;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index c2e08fc21..527d8edc6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -55,6 +55,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updatePermission(CrmPermissionUpdateReqBO updateBO) {
+        // TODO @puhui999:这里 1.1 1.2;下面 2.;这样更有序一点;
         // 1. 校验用户是否存在
         adminUserApi.validateUserList(Collections.singletonList(updateBO.getUserId()));
         // 2. 校验存在
@@ -91,7 +92,6 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         }
     }
 
-
     @Override
     public void transferPermission(CrmTransferPermissionReqBO transferReqBO) {
         // 1. 校验数据权限-是否是负责人,只有负责人才可以转移
@@ -103,6 +103,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
             throw exception(CRM_PERMISSION_DENIED, crmName);
         }
 
+        // TODO @puhui999:这个顺序编号,看看调整下;2. 后面是 2.1 ,结果没 2.2 有点怪;
         // 2. 校验转移对象是否已经是该负责人
         if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) {
             throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, crmName);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
index cff112b7e..fac0ef288 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
@@ -27,16 +27,15 @@ public class CrmPermissionCreateReqBO {
     @NotNull(message = "Crm 类型不能为空")
     @InEnum(CrmBizTypeEnum.class)
     private Integer bizType;
-
     /**
      * 数据编号
      */
     @NotNull(message = "Crm 数据编号不能为空")
     private Long bizId;
 
+    // TODO @puhui999:简化成 level
     /**
      * 权限级别
-     * 关联 {@link CrmPermissionLevelEnum}
      */
     @NotNull(message = "权限级别不能为空")
     @InEnum(CrmPermissionLevelEnum.class)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
index 72f94c46e..d3922a4d3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
@@ -17,7 +17,7 @@ import javax.validation.constraints.NotNull;
 public class CrmPermissionUpdateReqBO {
 
     /**
-     * 数据权限编号 {@link CrmPermissionDO#getId()}
+     * 数据权限编号
      */
     @NotNull(message = "Crm 数据权限编号不能为空")
     private Long id;
@@ -28,22 +28,22 @@ public class CrmPermissionUpdateReqBO {
     @NotNull(message = "用户编号不能为空")
     private Long userId;
 
+    // TODO @puhui999:id 字段,和 bizType + bizId 是否二选一;
     /**
      * Crm 类型
      */
     @NotNull(message = "Crm 类型不能为空")
     @InEnum(CrmBizTypeEnum.class)
     private Integer bizType;
-
     /**
      * 数据编号
      */
     @NotNull(message = "Crm 数据编号不能为空")
     private Long bizId;
 
+    // TODO @puhui999:简化成 level
     /**
      * 权限级别
-     * 关联 {@link CrmPermissionLevelEnum}
      */
     @NotNull(message = "权限级别不能为空")
     @InEnum(CrmPermissionLevelEnum.class)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java
index 3dce1cb57..d4cbdb43c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java
@@ -7,6 +7,7 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 
+// TODO @puhui999:CrmPermissionTransferReqBO
 /**
  * 数据权限转移 Request BO
  *
@@ -27,7 +28,6 @@ public class CrmTransferPermissionReqBO {
     @NotNull(message = "Crm 类型不能为空")
     @InEnum(CrmBizTypeEnum.class)
     private Integer bizType;
-
     /**
      * 数据编号
      */
@@ -40,6 +40,7 @@ public class CrmTransferPermissionReqBO {
     @NotNull(message = "新负责人的用户编号不能为空")
     private Long newOwnerUserId;
 
+    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
      * 老负责人是否加入团队,是/否
      */

From 0a58790c6b4363b1a3f538e8d3e76c012508edf7 Mon Sep 17 00:00:00 2001
From: Joey <zhouhangsir@163.com>
Date: Fri, 3 Nov 2023 22:07:47 +0800
Subject: [PATCH 059/101] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AE=A2=E6=88=B7?=
 =?UTF-8?q?=E9=94=81=E5=AE=9A=E5=92=8C=E8=A7=A3=E9=94=81=20=E6=8E=A5?=
 =?UTF-8?q?=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/admin/customer/CrmCustomerController.java | 8 ++++++++
 .../module/crm/service/customer/CrmCustomerService.java  | 7 +++++++
 .../crm/service/customer/CrmCustomerServiceImpl.java     | 9 +++++++++
 3 files changed, 24 insertions(+)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index ff144ffbf..0597c2292 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -95,4 +95,12 @@ public class CrmCustomerController {
         return success(true);
     }
 
+    @PutMapping("/lock")
+    @Operation(summary = "锁定/解锁")
+    @PreAuthorize("@ss.hasPermission('crm:customer:update')")
+    public CommonResult<Boolean> lockCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
+        customerService.lockCustomer(updateReqVO);
+        return success(true);
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index 576a9bcbc..fca534514 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -86,4 +86,11 @@ public interface CrmCustomerService {
      */
     void transferCustomer(CrmTransferCustomerReqVO reqVO, Long userId);
 
+    /**
+     * 锁定客户 解锁客户
+     *
+     * @param updateReqVO 更新信息
+     */
+    void lockCustomer(@Valid CrmCustomerUpdateReqVO updateReqVO);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index e78717b2c..6215eb271 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -138,4 +138,13 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
                 CrmCustomerConvert.INSTANCE.convert(reqVO, userId).setCrmType(CrmEnum.CRM_CUSTOMER.getType()));
     }
 
+    @Override
+    public void lockCustomer(CrmCustomerUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateCustomerExists(updateReqVO.getId());
+        // 更新
+        CrmCustomerDO updateObj = CrmCustomerConvert.INSTANCE.convert(updateReqVO);
+        customerMapper.updateById(updateObj);
+    }
+
 }

From b7900c2035d23df2581d37de1d7f0d8845de8a26 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 3 Nov 2023 23:02:01 +0800
Subject: [PATCH 060/101] =?UTF-8?q?code=20review=EF=BC=9A=E5=9B=9E?=
 =?UTF-8?q?=E6=AC=BE=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/AuditStatusEnum.java     | 12 ++++----
 .../module/crm/enums/ReturnTypeEnum.java      |  2 +-
 .../receivable/CrmReceivableController.java   |  1 +
 .../receivable/vo/CrmReceivableBaseVO.java    |  8 ++++-
 .../receivable/vo/CrmReceivableExcelVO.java   | 16 ++++------
 .../vo/CrmReceivableExportReqVO.java          |  4 +--
 .../receivable/vo/CrmReceivablePageReqVO.java |  2 ++
 .../vo/CrmReceivablePlanBaseVO.java           |  4 +++
 .../vo/CrmReceivablePlanExcelVO.java          | 11 ++++---
 .../vo/CrmReceivablePlanExportReqVO.java      |  5 ++--
 .../vo/CrmReceivablePlanPageReqVO.java        |  2 ++
 .../receivable/CrmReceivableDO.java           | 11 +++++--
 .../receivable/CrmReceivablePlanDO.java       | 29 ++++++++++++++-----
 .../CrmReceivablePlanServiceImpl.java         |  1 +
 .../receivable/CrmReceivableServiceImpl.java  | 10 +++++--
 15 files changed, 75 insertions(+), 43 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
index 6cc1012b6..85236fdd2 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
@@ -4,12 +4,16 @@ import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 
 import java.util.Arrays;
 
+// TODO @liuhongfeng:这个状态,还是搞成专属 CrmReceivableDO 专属的 status;
 /**
  * 流程审批状态枚举类
- * 0 未审核 1 审核通过 2 审核拒绝 3 审核中 4 已撤回
+ * 0 未审核 1 审核通过 2 审核拒绝 3 审核中 4 已撤回 TODO @liuhongfeng:这一行可以删除,因为已经有枚举属性了哈;
  * @author 赤焰
  */
+// TODO @liuhongfeng:可以使用 @Getter、@AllArgsConstructor 简化 get、构造方法
 public enum AuditStatusEnum implements IntArrayValuable {
+
+    // TODO @liuhongfeng:草稿 0;10 审核中;20 审核通过;30 审核拒绝;40 已撤回;主要是留好间隙,万一每个地方要做点拓展; 然后,枚举字段的顺序调整下,审批中,一定要放两个审批通过、拒绝前面哈;
     /**
      * 未审批
      */
@@ -31,16 +35,12 @@ public enum AuditStatusEnum implements IntArrayValuable {
 	 */
 	AUDIT_RETURN(4, "已撤回");
 
+    // TODO liuhongfeng:value 改成 status;desc 改成 name;
     private final Integer value;
     private final String desc;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AuditStatusEnum::getValue).toArray();
 
-    /**
-     *
-     * @param value
-     * @param desc
-     */
     AuditStatusEnum(Integer value, String desc) {
         this.value = value;
         this.desc = desc;
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java
index d9ca477be..e6074c432 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java
@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.crm.enums;
 
+// TODO @liuhongfeng:这个的作用是?
 /**
  * @author 赤焰
  */
-
 public enum ReturnTypeEnum {
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
index 4f7d9e674..490071900 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
@@ -30,6 +30,7 @@ import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.E
 @Validated
 public class CrmReceivableController {
 
+    // TODO @liuhongfeng:crmReceivableService 可以使用 receivableService ;在自己模块里,相对简洁一点;
     @Resource
     private CrmReceivableService crmReceivableService;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableBaseVO.java
index 24b1d57e4..7b3ffd881 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableBaseVO.java
@@ -20,15 +20,19 @@ public class CrmReceivableBaseVO {
     @Schema(description = "回款编号",requiredMode = Schema.RequiredMode.REQUIRED, example = "31177")
     private String no;
 
+    // TODO @liuhongfeng:回款计划编号
     @Schema(description = "回款计划", example = "31177")
     private Long planId;
 
+    // TODO @liuhongfeng:客户编号
     @Schema(description = "客户名称", example = "4963")
     private Long customerId;
 
+    // TODO @liuhongfeng:客户编号
     @Schema(description = "合同名称", example = "30305")
     private Long contractId;
 
+    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的
     @Schema(description = "审批状态", example = "1")
     @InEnum(AuditStatusEnum.class)
     private Integer checkStatus;
@@ -40,9 +44,10 @@ public class CrmReceivableBaseVO {
     @Schema(description = "回款方式", example = "2")
     private String returnType;
 
-    @Schema(description = "回款金额", example = "31859")
+    @Schema(description = "回款金额,单位:分", example = "31859")
     private Integer price;
 
+    // TODO @liuhongfeng:负责人编号
     @Schema(description = "负责人", example = "22202")
     private Long ownerUserId;
 
@@ -55,6 +60,7 @@ public class CrmReceivableBaseVO {
     @Schema(description = "备注", example = "备注")
     private String remark;
 
+    // TODO @liuhongfeng:这个字段,这个字段,应该不是前端传递的噢,而是后端自己生成的,所以不适合放在 base 里面;
     @Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     private Integer status;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java
index 1bd461499..291431e2c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java
@@ -1,20 +1,14 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
-import lombok.*;
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
 
 import java.time.LocalDateTime;
 
-import com.alibaba.excel.annotation.ExcelProperty;
-import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
-import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
-
-
-/**
- * CRM 回款管理 Excel VO
- *
- * @author 赤焰
- */
+// TODO liuhongfeng:导出可以等其它功能做完,统一在搞;
 @Data
 public class CrmReceivableExcelVO {
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java
index b674bbc2b..c8bdb16e7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java
@@ -8,9 +8,7 @@ import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-/**
- * @author 赤焰
- */
+// TODO liuhongfeng:导出可以等其它功能做完,统一在搞;
 @Schema(description = "管理后台 - CRM 回款 Excel 导出 Request VO,参数和 CrmReceivablePageReqVO 是一致的")
 @Data
 public class CrmReceivableExportReqVO {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java
index 3a90ed809..61bd6f9a5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java
@@ -17,6 +17,8 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class CrmReceivablePageReqVO extends PageParam {
 
+    // TODO @liuhongfeng:可以根据需求,去除掉一些不要的过滤条件;另外,planId、customerId、contractId、ownerUserId 注释不正确,应该都是对应的编号
+
     @Schema(description = "回款编号")
     private String no;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanBaseVO.java
index 49d00892e..eaba43dce 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanBaseVO.java
@@ -20,6 +20,7 @@ public class CrmReceivablePlanBaseVO {
     @Schema(description = "期数", example = "1")
     private Integer period;
 
+    // TODO @liuhongfeng:回款计划编号
     @Schema(description = "回款计划", example = "19852")
     private Long receivableId;
 
@@ -44,12 +45,15 @@ public class CrmReceivablePlanBaseVO {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime remindTime;
 
+    // TODO @liuhongfeng:客户编号
     @Schema(description = "客户名称", example = "18026")
     private Long customerId;
 
+    // TODO @liuhongfeng:合同编号
     @Schema(description = "合同名称", example = "3473")
     private Long contractId;
 
+    // TODO @liuhongfeng:负责人编号
     @Schema(description = "负责人", example = "17828")
     private Long ownerUserId;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java
index a7246e252..f4dd28366 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java
@@ -1,15 +1,14 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
-import lombok.*;
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
 
 import java.time.LocalDateTime;
 
-import com.alibaba.excel.annotation.ExcelProperty;
-import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
-import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
-
-
+// TODO liuhongfeng:导出可以等其它功能做完,统一在搞;
 /**
  * CRM 回款计划 Excel VO
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java
index 8002d41af..cbe86bd05 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java
@@ -1,13 +1,14 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
 
-import lombok.*;
 import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
 
 import java.time.LocalDateTime;
-import org.springframework.format.annotation.DateTimeFormat;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
+// TODO liuhongfeng:导出可以等其它功能做完,统一在搞;
 @Schema(description = "管理后台 - CRM 回款计划 Excel 导出 Request VO,参数和 CrmReceivablePlanPageReqVO 是一致的")
 @Data
 public class CrmReceivablePlanExportReqVO {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanPageReqVO.java
index 10e26207a..a510753dd 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanPageReqVO.java
@@ -17,6 +17,8 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class CrmReceivablePlanPageReqVO extends PageParam {
 
+    // TODO @liuhongfeng:可以根据需求,去除掉一些不要的过滤条件;另外,customerId、contractId、ownerUserId 注释不正确,应该都是对应的编号
+
     @Schema(description = "完成状态", example = "2")
     private Integer status;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
index b74507dd1..a8c3e3fed 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
@@ -32,24 +32,28 @@ public class CrmReceivableDO extends BaseDO {
      * 回款编号
      */
     private String no;
+    // TODO @liuhongfeng:“对应实体”,参考别的模块,关联 {@link TableField.MetaInfo#getJdbcType()}
     /**
      * 回款计划
      *
+     * TODO @liuhongfeng:这个字段什么时候更新,也可以写下
+     *
      * 对应实体 {@link CrmReceivablePlanDO}
      */
     private Long planId;
     /**
-     * 客户ID
+     * 客户 ID
      *
      * 对应实体 {@link cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO}
      */
     private Long customerId;
     /**
-     * 合同ID
+     * 合同 ID
      *
      * 对应实体 {@link cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO}
      */
     private Long contractId;
+    // TODO @liuhongfeng:“对应字典”,参考别的模块,枚举 {@link XXXX};另外,这个字段就叫 status,整体状态,不只审批
     /**
      * 审批状态
      * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_RECEIVABLE_CHECK_STATUS}
@@ -65,6 +69,7 @@ public class CrmReceivableDO extends BaseDO {
      * 回款日期
      */
     private LocalDateTime returnTime;
+    // TODO @liuhongfeng:少个枚举
     /**
      * 回款方式
      */
@@ -73,10 +78,12 @@ public class CrmReceivableDO extends BaseDO {
      * 回款金额
      */
     private Integer price;
+    // TODO @liuhongfeng:少关联实体;
     /**
      * 负责人
      */
     private Long ownerUserId;
+    // TODO @liuhongfeng:应该不需要 batchId 字段
     /**
      * 批次
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java
index 4274250e8..52c0d2745 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java
@@ -1,12 +1,13 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.receivable;
 
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 
 import java.time.LocalDateTime;
 
-import com.baomidou.mybatisplus.annotation.*;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-
 /**
  * 回款计划 DO
  *
@@ -33,26 +34,32 @@ public class CrmReceivablePlanDO extends BaseDO {
     private Integer period;
     /**
      * 回款ID
+     *
+     * TODO @liuhongfeng:少关联实体;
      */
     private Long receivableId;
+    // TODO @liuhongfeng:还款计划,没有 status 和 checkStatus,改成 finishStatus Boolean;是否完成
     /**
      * 状态
      *
      * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
-     *
      */
     private Integer status;
     /**
      * 审批状态
+     *
      * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_RECEIVABLE_CHECK_STATUS}
+     * // TODO @liuhongfeng:关联的枚举
      */
     private Integer checkStatus;
     /**
      * 工作流编号
+     *
+     * TODO @liuhongfeng:少关联实体;
      */
     private Long processInstanceId;
     /**
-     * 计划回款金额
+     * 计划回款金额,单位:分
      */
     private Integer price;
     /**
@@ -68,15 +75,21 @@ public class CrmReceivablePlanDO extends BaseDO {
      */
     private LocalDateTime remindTime;
     /**
-     * 客户ID
+     * 客户 ID
+     *
+     * TODO @liuhongfeng:少关联实体;
      */
     private Long customerId;
     /**
-     * 合同ID
+     * 合同 ID
+     *
+     * TODO @liuhongfeng:少关联实体;
      */
     private Long contractId;
     /**
-     * 负责人
+     * 负责人 ID
+     *
+     * TODO @liuhongfeng:少关联实体;
      */
     private Long ownerUserId;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
index b3497b686..30523a095 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
@@ -27,6 +27,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.*;
 
+// TODO @liuhongfeng:参考 CrmReceivableServiceImpl 写的 todo 哈;
 /**
  * 回款计划 Service 实现类
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
index ff751bc51..08d97d8fe 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -37,6 +37,7 @@ import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 @Validated
 public class CrmReceivableServiceImpl implements CrmReceivableService {
 
+    // TODO @liuhongfeng:crm 前缀,变量可以不带哈;
     @Resource
     private CrmReceivableMapper crmReceivableMapper;
     @Resource
@@ -46,10 +47,12 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     @Resource
     private CrmReceivablePlanService crmReceivablePlanService;
 
+    // TODO @liuhongfeng:创建还款后,是不是什么时候,要更新 plan?
     @Override
     public Long createReceivable(CrmReceivableCreateReqVO createReqVO) {
         // 插入
         CrmReceivableDO receivable = CrmReceivableConvert.INSTANCE.convert(createReqVO);
+        // TODO @liuhongfeng:这里的括号要注意排版;
         if (ObjectUtil.isNull(receivable.getStatus())){
             receivable.setStatus(CommonStatusEnum.ENABLE.getStatus());
         }
@@ -57,16 +60,17 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
             receivable.setCheckStatus(AuditStatusEnum.AUDIT_NEW.getValue());
         }
 
-        //校验
+        // TODO @liuhongfeng:一般来说,逻辑的写法,是要先检查,后操作 db;所以,你这个 check 应该放到  CrmReceivableDO receivable 之前;
+        // 校验
         checkReceivable(receivable);
 
         crmReceivableMapper.insert(receivable);
-        // 返回
         return receivable.getId();
     }
 
+    // TODO @liuhongfeng:这里的括号要注意排版;
     private void checkReceivable(CrmReceivableDO receivable) {
-
+        // TODO @liuhongfeng:这个放在参数校验合适
         if(ObjectUtil.isNull(receivable.getContractId())){
             throw exception(CONTRACT_NOT_EXISTS);
         }

From acdfc28ee6ce6b8fc659fad1a5e36868563c1c78 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 3 Nov 2023 23:43:04 +0800
Subject: [PATCH 061/101] =?UTF-8?q?code=20review=EF=BC=9A=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E9=94=81=E5=AE=9A=E7=9A=84=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../crm/controller/admin/customer/CrmCustomerController.java   | 3 ++-
 .../yudao/module/crm/service/customer/CrmCustomerService.java  | 2 +-
 .../module/crm/service/customer/CrmCustomerServiceImpl.java    | 3 +++
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index ddcf6e97e..14dedecd1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -95,8 +95,9 @@ public class CrmCustomerController {
         return success(true);
     }
 
+    // TODO @Joey:单独建一个属于自己业务的 ReqVO;因为前端如果模拟请求,是不是可以更新其它字段了;
     @PutMapping("/lock")
-    @Operation(summary = "锁定/解锁")
+    @Operation(summary = "锁定/解锁客户")
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")
     public CommonResult<Boolean> lockCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
         customerService.lockCustomer(updateReqVO);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index 421113bd8..a9c47a0d4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -87,7 +87,7 @@ public interface CrmCustomerService {
     void transferCustomer(CrmCustomerTransferReqVO reqVO, Long userId);
 
     /**
-     * 锁定客户 解锁客户
+     * 锁定/解锁客户
      *
      * @param updateReqVO 更新信息
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 69d68cc05..819fbc870 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -146,6 +146,9 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     public void lockCustomer(CrmCustomerUpdateReqVO updateReqVO) {
         // 校验存在
         validateCustomerExists(updateReqVO.getId());
+        // TODO @Joey:可以校验下,如果已经对应的锁定状态,报个业务异常;原因是:后续这个业务会记录操作日志,会记录多了;
+        // TODO @芋艿:业务完善,增加锁定上限;
+
         // 更新
         CrmCustomerDO updateObj = CrmCustomerConvert.INSTANCE.convert(updateReqVO);
         customerMapper.updateById(updateObj);

From 64db1a9c908072bdac4ca40cee55682307642a52 Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Sat, 4 Nov 2023 03:21:20 +0800
Subject: [PATCH 062/101] =?UTF-8?q?feat:=20=E5=AE=A2=E6=88=B7=E8=AF=A6?=
 =?UTF-8?q?=E6=83=85=20+=20review=20=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../enums/customer/CrmCustomerLevelEnum.java  |  5 +-
 yudao-module-crm/yudao-module-crm-biz/pom.xml |  6 +++
 .../admin/customer/CrmCustomerController.java | 51 +++++++++++++++++--
 .../admin/customer/vo/CrmCustomerBaseVO.java  |  2 +-
 .../customer/vo/CrmCustomerCreateReqVO.java   |  6 ++-
 .../admin/customer/vo/CrmCustomerExcelVO.java |  2 +-
 .../customer/vo/CrmCustomerPageReqVO.java     |  9 ++++
 .../admin/customer/vo/CrmCustomerRespVO.java  | 17 +++++++
 .../dataobject/customer/CrmCustomerDO.java    | 14 ++++-
 .../dal/mysql/customer/CrmCustomerMapper.java |  4 +-
 .../service/customer/CrmCustomerService.java  |  8 ---
 .../customer/CrmCustomerServiceImpl.java      | 12 -----
 12 files changed, 105 insertions(+), 31 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
index f58028314..aa06b05eb 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
@@ -19,13 +19,12 @@ public enum CrmCustomerLevelEnum implements IntArrayValuable {
     GENERAL(2, "B(普通客户)"),
     LOW_PRIORITY(3, "C(非优先客户)");
 
-    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLevelEnum::getStatus).toArray();
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLevelEnum::getLevel).toArray();
 
-    // TODO @wanwan:这里的 status 字段,可以考虑改成 level
     /**
      * 状态
      */
-    private final Integer status;
+    private final Integer level;
     /**
      * 状态名
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/pom.xml b/yudao-module-crm/yudao-module-crm-biz/pom.xml
index 5bedf5eac..bb179e592 100644
--- a/yudao-module-crm/yudao-module-crm-biz/pom.xml
+++ b/yudao-module-crm/yudao-module-crm-biz/pom.xml
@@ -57,6 +57,12 @@
             <artifactId>yudao-spring-boot-starter-excel</artifactId>
         </dependency>
 
+        <!-- 地址相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
+        </dependency>
+
         <!-- Test 测试相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index c44c12b70..6a2cbc660 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -1,13 +1,22 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import com.google.common.collect.Lists;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -19,7 +28,9 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.IOException;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@@ -33,6 +44,10 @@ public class CrmCustomerController {
 
     @Resource
     private CrmCustomerService customerService;
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private AdminUserApi adminUserApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建客户")
@@ -64,7 +79,21 @@ public class CrmCustomerController {
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<CrmCustomerRespVO> getCustomer(@RequestParam("id") Long id) {
         CrmCustomerDO customer = customerService.getCustomer(id);
-        return success(CrmCustomerConvert.INSTANCE.convert(customer));
+        CrmCustomerRespVO customerRespVO = CrmCustomerConvert.INSTANCE.convert(customer);
+        if (ObjectUtil.isAllNotEmpty(customer, customer.getAreaId())) {
+            customerRespVO.setAreaName(AreaUtils.format(customer.getAreaId()));
+        }
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(CollUtil.removeNull(Lists.newArrayList(NumberUtil.parseLong(customerRespVO.getCreator()), customerRespVO.getOwnerUserId())));
+        customerRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(customerRespVO.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
+        AdminUserRespDTO ownerUser = userMap.get(customer.getOwnerUserId());
+        if (Objects.nonNull(ownerUser)) {
+            customerRespVO.setOwnerUserName(ownerUser.getNickname());
+            DeptRespDTO dept = deptApi.getDept(ownerUser.getDeptId());
+            if (Objects.nonNull(dept)) {
+                customerRespVO.setOwnerUserDept(dept.getName());
+            }
+        }
+        return success(customerRespVO);
     }
 
     @GetMapping("/page")
@@ -72,7 +101,23 @@ public class CrmCustomerController {
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
         PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO);
-        return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult));
+        PageResult<CrmCustomerRespVO> pageVo = CrmCustomerConvert.INSTANCE.convertPage(pageResult);
+        Set<Long> userSet = pageVo.getList().stream().flatMap(i -> Stream.of(NumberUtil.parseLong(i.getCreator()), i.getOwnerUserId())).collect(Collectors.toSet());
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userSet);
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(userMap.values().stream().map(AdminUserRespDTO::getDeptId).collect(Collectors.toSet()));
+        pageVo.getList().forEach(customerRespVO -> {
+            customerRespVO.setAreaName(AreaUtils.format(customerRespVO.getAreaId()));
+            customerRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(customerRespVO.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
+            AdminUserRespDTO ownerUser = userMap.get(customerRespVO.getOwnerUserId());
+            if (Objects.nonNull(ownerUser)) {
+                customerRespVO.setOwnerUserName(ownerUser.getNickname());
+                DeptRespDTO dept = deptMap.get(ownerUser.getDeptId());
+                if (Objects.nonNull(dept)) {
+                    customerRespVO.setOwnerUserDept(dept.getName());
+                }
+            }
+        });
+        return success(pageVo);
     }
 
     @GetMapping("/export-excel")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
index 0e4d4463d..c654b4b56 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
@@ -68,7 +68,7 @@ public class CrmCustomerBaseVO {
     private String remark;
 
     @Schema(description = "地区编号", example = "20158")
-    private Long areaId;
+    private Integer areaId;
 
     @Schema(description = "详细地址", example = "北京市海淀区")
     private String detailAddress;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
index 38108776a..41324079d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
@@ -5,12 +5,16 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
+import javax.validation.constraints.NotNull;
+
 @Schema(description = "管理后台 - CRM 客户创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class CrmCustomerCreateReqVO extends CrmCustomerBaseVO {
 
-    // TODO @wanwan:负责人
+    @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    @NotNull(message = "负责人不能为空")
+    private Long ownerUserId;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
index 6f8156175..d49f569b3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
@@ -76,7 +76,7 @@ public class CrmCustomerExcelVO {
     private Long ownerUserId;
 
     @ExcelProperty("地区编号")
-    private Long areaId;
+    private Integer areaId;
 
     @ExcelProperty("详细地址")
     private String detailAddress;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
index e8a0b9e71..c34980b36 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
@@ -18,5 +18,14 @@ public class CrmCustomerPageReqVO extends PageParam {
     @Schema(description = "手机", example = "18000000000")
     private String mobile;
 
+    @Schema(description = "所属行业", example = "1")
+    private Integer industryId;
+
+    @Schema(description = "客户等级", example = "1")
+    private Integer level;
+
+    @Schema(description = "客户来源", example = "1")
+    private Integer source;
+
     // TODO @芋艿:场景;
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
index 505221dfd..1bda33a3c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
@@ -31,6 +31,15 @@ public class CrmCustomerRespVO extends CrmCustomerBaseVO {
     @Schema(description = "负责人的用户编号", example = "25682")
     private Long ownerUserId;
 
+    @Schema(description = "负责人名字", example = "25682")
+    private String ownerUserName;
+
+    @Schema(description = "负责人部门")
+    private String ownerUserDept;
+
+    @Schema(description = "地区名称", example = "北京市")
+    private String areaName;
+
     @Schema(description = "最后跟进时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactLastTime;
@@ -38,4 +47,12 @@ public class CrmCustomerRespVO extends CrmCustomerBaseVO {
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 
+    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime updateTime;
+
+    @Schema(description = "创建人")
+    private String creator;
+
+    @Schema(description = "创建人名字")
+    private String creatorName;
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index 9de87ee5b..8b34ed582 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -96,10 +96,22 @@ public class CrmCustomerDO extends BaseDO {
      * 备注
      */
     private String remark;
+    /**
+     * 负责人的用户编号
+     */
+    private Long ownerUserId;
+    /**
+     * 只读权限的用户编号数组
+     */
+    private String roUserIds;
+    /**
+     * 读写权限的用户编号数组
+     */
+    private String rwUserIds;
     /**
      * 地区编号
      */
-    private Long areaId;
+    private Integer areaId;
     /**
      * 详细地址
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
index 0b2497a7b..74cdfe5dd 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -19,10 +19,12 @@ import java.util.List;
 public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
 
     default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO reqVO) {
-        // TODO @Wanwan 填充负责人,所属部门字段;这个可以在 controller 填哈;
         return selectPage(reqVO, new LambdaQueryWrapperX<CrmCustomerDO>()
                 .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
                 .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
+                .eqIfPresent(CrmCustomerDO::getIndustryId, reqVO.getIndustryId())
+                .eqIfPresent(CrmCustomerDO::getLevel, reqVO.getLevel())
+                .eqIfPresent(CrmCustomerDO::getSource, reqVO.getSource())
                 .orderByDesc(CrmCustomerDO::getId));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index 5f64142b2..2c12d5d92 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -46,14 +46,6 @@ public interface CrmCustomerService {
      */
     CrmCustomerDO getCustomer(Long id);
 
-    /**
-     * 获得客户列表
-     *
-     * @param ids 编号
-     * @return 客户列表
-     */
-    List<CrmCustomerDO> getCustomerList(Collection<Long> ids);
-
     /**
      * 获得客户分页
      *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index e77ceb042..5121abe80 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -37,8 +37,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     @Resource
     private CrmCustomerMapper customerMapper;
     @Resource
-    private DeptApi deptApi; // TODO @wanwan:拼接数据,可以放到 controller;所以这里的引入,可以考虑放到 controller 哈;
-    @Resource
     private CrmPermissionService crmPermissionService;
 
     @Override
@@ -93,14 +91,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         return customerMapper.selectById(id);
     }
 
-    @Override
-    public List<CrmCustomerDO> getCustomerList(Collection<Long> ids) {
-        if (CollUtil.isEmpty(ids)) {
-            return ListUtil.empty();
-        }
-        return customerMapper.selectBatchIds(ids);
-    }
-
     @Override
     public PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO) {
         // TODO 芋艿:数据权限,是否可以查询到;
@@ -112,8 +102,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         return customerMapper.selectList(exportReqVO);
     }
 
-    // TODO wanwan:service 接口已经注释,实现类就不需要了。
-
     /**
      * 校验客户是否存在
      *

From ecf728966fb4ac33eb0f570e01e5824462793fed Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 4 Nov 2023 10:13:43 +0800
Subject: [PATCH 063/101] =?UTF-8?q?code=20review=EF=BC=9A=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 yudao-module-crm/yudao-module-crm-biz/pom.xml          | 10 ++++------
 .../admin/customer/CrmCustomerController.java          |  3 +++
 .../admin/customer/vo/CrmCustomerRespVO.java           |  4 +---
 .../crm/dal/dataobject/customer/CrmCustomerDO.java     |  3 +++
 .../crm/framework/core/aop/CrmPermissionAspect.java    |  4 ++++
 5 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/pom.xml b/yudao-module-crm/yudao-module-crm-biz/pom.xml
index bb179e592..15bbc932d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/pom.xml
+++ b/yudao-module-crm/yudao-module-crm-biz/pom.xml
@@ -33,6 +33,10 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
+        </dependency>
 
         <!-- Web 相关 -->
         <dependency>
@@ -57,12 +61,6 @@
             <artifactId>yudao-spring-boot-starter-excel</artifactId>
         </dependency>
 
-        <!-- 地址相关 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
-        </dependency>
-
         <!-- Test 测试相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 8624ca68e..10b6b5ee8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -102,12 +102,15 @@ public class CrmCustomerController {
     public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
         PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO);
         PageResult<CrmCustomerRespVO> pageVo = CrmCustomerConvert.INSTANCE.convertPage(pageResult);
+        // TODO @wanwan: 可以参考 CollectionUtils.convertListByFlatMap(),目的是简洁
         Set<Long> userSet = pageVo.getList().stream().flatMap(i -> Stream.of(NumberUtil.parseLong(i.getCreator()), i.getOwnerUserId())).collect(Collectors.toSet());
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userSet);
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(userMap.values().stream().map(AdminUserRespDTO::getDeptId).collect(Collectors.toSet()));
+        // TODO @wanwan:这块可以形成一个 convertPage 方法,default 实现;
         pageVo.getList().forEach(customerRespVO -> {
             customerRespVO.setAreaName(AreaUtils.format(customerRespVO.getAreaId()));
             customerRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(customerRespVO.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
+            // TODO @wanwan:可以使用 MapUtils.findAndThen
             AdminUserRespDTO ownerUser = userMap.get(customerRespVO.getOwnerUserId());
             if (Objects.nonNull(ownerUser)) {
                 customerRespVO.setOwnerUserName(ownerUser.getNickname());
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
index 1bda33a3c..f37740f26 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
@@ -30,10 +30,8 @@ public class CrmCustomerRespVO extends CrmCustomerBaseVO {
 
     @Schema(description = "负责人的用户编号", example = "25682")
     private Long ownerUserId;
-
     @Schema(description = "负责人名字", example = "25682")
     private String ownerUserName;
-
     @Schema(description = "负责人部门")
     private String ownerUserDept;
 
@@ -52,7 +50,7 @@ public class CrmCustomerRespVO extends CrmCustomerBaseVO {
 
     @Schema(description = "创建人")
     private String creator;
-
     @Schema(description = "创建人名字")
     private String creatorName;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index 8b34ed582..3e00b1cc5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -98,8 +98,11 @@ public class CrmCustomerDO extends BaseDO {
     private String remark;
     /**
      * 负责人的用户编号
+     *
+     * 关联 AdminUserDO 的 id 字段
      */
     private Long ownerUserId;
+    // TODO @puhui999:这块抽到 permission 里;
     /**
      * 只读权限的用户编号数组
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index 8a60b6d45..497c7caba 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -65,6 +65,10 @@ public class CrmPermissionAspect {
     // TODO @puhui999:一般核心的方法,放到最前面,private 放后面。主要是,主次要分出来哈;
     @Before("@annotation(crmPermission)")
     public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
+        // TODO 芋艿:临时,方便大家调试
+        if (true) {
+            return;
+        }
         try {
             Long bizId = crmPermission.getIdFor().length > 0 ? getBizId(joinPoint, crmPermission) : (Long) joinPoint.getArgs()[0]; // 获取操作数据的编号
             Integer bizType = crmPermission.bizType().getType(); // 模块类型

From bf42f8e9ff7dbe2645523c682c7f2239e6e5f08e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?ZanGe=E4=B8=B6?= <385454831@qq.com>
Date: Sat, 4 Nov 2023 19:48:20 +0800
Subject: [PATCH 064/101] =?UTF-8?q?=E3=80=90=E5=A2=9E=E5=8A=A0CRM=EF=BC=9A?=
 =?UTF-8?q?=E4=BA=A7=E5=93=81=E4=BB=A5=E5=8F=8A=E4=BA=A7=E5=93=81=E5=88=86?=
 =?UTF-8?q?=E7=B1=BB=E3=80=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             |  42 +++++++
 sql/mysql/crm_data.sql                        |   4 +
 .../module/crm/enums/ErrorCodeConstants.java  |   7 ++
 .../admin/product/ProductController.java      | 102 +++++++++++++++++
 .../admin/product/vo/ProductBaseVO.java       |  46 ++++++++
 .../admin/product/vo/ProductCreateReqVO.java  |  14 +++
 .../admin/product/vo/ProductExcelVO.java      |  53 +++++++++
 .../admin/product/vo/ProductExportReqVO.java  |  44 ++++++++
 .../admin/product/vo/ProductPageReqVO.java    |  46 ++++++++
 .../admin/product/vo/ProductRespVO.java       |  19 ++++
 .../admin/product/vo/ProductUpdateReqVO.java  |  18 +++
 .../ProductCategoryController.java            |  81 ++++++++++++++
 .../vo/ProductCategoryBaseVO.java             |  25 +++++
 .../vo/ProductCategoryCreateReqVO.java        |  14 +++
 .../vo/ProductCategoryListReqVO.java          |  27 +++++
 .../vo/ProductCategoryRespVO.java             |  19 ++++
 .../vo/ProductCategoryUpdateReqVO.java        |  18 +++
 .../crm/convert/product/ProductConvert.java   |  34 ++++++
 .../ProductCategoryConvert.java               |  30 +++++
 .../crm/dal/dataobject/product/ProductDO.java |  65 +++++++++++
 .../productcategory/ProductCategoryDO.java    |  39 +++++++
 .../crm/dal/mysql/product/ProductMapper.java  |  48 ++++++++
 .../ProductCategoryMapper.java                |  28 +++++
 .../crm/service/product/ProductService.java   |  70 ++++++++++++
 .../service/product/ProductServiceImpl.java   | 104 ++++++++++++++++++
 .../ProductCategoryService.java               |  54 +++++++++
 .../ProductCategoryServiceImpl.java           |  75 +++++++++++++
 .../mapper/product/ProductMapper.xml          |  12 ++
 .../productcategory/ProductCategoryMapper.xml |  12 ++
 29 files changed, 1150 insertions(+)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/ProductController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExcelVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExportReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductPageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/ProductCategoryController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryListReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/product/ProductConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/productcategory/ProductCategoryConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/ProductDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/productcategory/ProductCategoryDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/product/ProductMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/productcategory/ProductCategoryMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/product/ProductMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/productcategory/ProductCategoryMapper.xml

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index 8c662c238..b11091fbe 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -63,3 +63,45 @@ CREATE TABLE `crm_receivable_plan`  (
     `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
     PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回款计划' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- 产品表
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_product`;
+CREATE TABLE `crm_product`
+(
+    `id`            bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
+    `name`          varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品名称',
+    `no`            varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci  NOT NULL COMMENT '产品编码',
+    `unit`          varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '单位',
+    `price`         bigint(20) NULL DEFAULT 0 COMMENT '价格',
+    `status`        tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态 1-上架 0-下架',
+    `category_id`   bigint(20) NOT NULL COMMENT '产品分类ID',
+    `description`   varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '产品描述',
+    `owner_user_id` bigint(20) NOT NULL COMMENT '负责人的用户编号',
+    `creator`       varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+    `create_time`   datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updater`       varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+    `update_time`   datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `tenant_id`     bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
+    `deleted`       bit(1)                                                        NOT NULL DEFAULT b'0' COMMENT '是否删除',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '产品表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- 产品分类表
+-- ----------------------------
+DROP TABLE IF EXISTS `crm_product_category`;
+CREATE TABLE `crm_product_category`
+(
+    `id`          bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
+    `name`        varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
+    `parent_id`   bigint(20) NOT NULL COMMENT '父级id',
+    `creator`     varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+    `create_time` datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updater`     varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+    `update_time` datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `tenant_id`   bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
+    `deleted`     bit(1)                                                        NOT NULL DEFAULT b'0' COMMENT '是否删除',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '产品分类表' ROW_FORMAT = Dynamic;
\ No newline at end of file
diff --git a/sql/mysql/crm_data.sql b/sql/mysql/crm_data.sql
index 2d742698d..149c65268 100644
--- a/sql/mysql/crm_data.sql
+++ b/sql/mysql/crm_data.sql
@@ -17,4 +17,8 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1400, 7, '微信支付', '7', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:53', '1', '2023-10-18 21:55:53', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1401, 8, '其他', '8', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:56:06', '1', '2023-10-18 21:56:06', b'0');
 
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (604, '产品状态', 'crm_product_status', 0, '', '1', '2023-10-30 21:47:59', '1', '2023-10-30 21:48:45', b'0', '1970-01-01 00:00:00');
+
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1412, 1, '上架', '1', 'crm_product_status', 0, 'success', '', '', '1', '2023-10-30 21:49:34', '1', '2023-10-30 21:49:34', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1411, 0, '下架', '0', 'crm_product_status', 0, 'success', '', '', '1', '2023-10-30 21:49:13', '1', '2023-10-30 21:49:13', b'0');
 
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 3a6e3713b..36767f83a 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -41,4 +41,11 @@ public interface ErrorCodeConstants {
     ErrorCode CRM_PERMISSION_MODEL_NOT_EXISTS = new ErrorCode(1_020_007_002, "{}不存在");
     ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_007_003, "{}操作失败,原因:转移对象已经是该负责人");
 
+    // ========== 产品 1_020_008_000 ==========
+    ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_020_008_000, "产品不存在");
+    ErrorCode PRODUCT_NO_EXISTS = new ErrorCode(1_020_008_001, "产品编号已存在");
+
+    // ========== 产品分类 1_020_009_000 ==========
+    ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_020_009_000, "产品分类不存在");
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/ProductController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/ProductController.java
new file mode 100644
index 000000000..da55dd0b6
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/ProductController.java
@@ -0,0 +1,102 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.crm.controller.admin.product.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.product.ProductDO;
+import cn.iocoder.yudao.module.crm.convert.product.ProductConvert;
+import cn.iocoder.yudao.module.crm.service.product.ProductService;
+
+@Tag(name = "管理后台 - 产品")
+@RestController
+@RequestMapping("/crm/product")
+@Validated
+public class ProductController {
+
+    @Resource
+    private ProductService productService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建产品")
+    @PreAuthorize("@ss.hasPermission('crm:product:create')")
+    public CommonResult<Long> createProduct(@Valid @RequestBody ProductCreateReqVO createReqVO) {
+        return success(productService.createProduct(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新产品")
+    @PreAuthorize("@ss.hasPermission('crm:product:update')")
+    public CommonResult<Boolean> updateProduct(@Valid @RequestBody ProductUpdateReqVO updateReqVO) {
+        productService.updateProduct(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除产品")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:product:delete')")
+    public CommonResult<Boolean> deleteProduct(@RequestParam("id") Long id) {
+        productService.deleteProduct(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得产品")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:product:query')")
+    public CommonResult<ProductRespVO> getProduct(@RequestParam("id") Long id) {
+        ProductDO product = productService.getProduct(id);
+        return success(ProductConvert.INSTANCE.convert(product));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得产品列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:product:query')")
+    public CommonResult<List<ProductRespVO>> getProductList(@RequestParam("ids") Collection<Long> ids) {
+        List<ProductDO> list = productService.getProductList(ids);
+        return success(ProductConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得产品分页")
+    @PreAuthorize("@ss.hasPermission('crm:product:query')")
+    public CommonResult<PageResult<ProductRespVO>> getProductPage(@Valid ProductPageReqVO pageVO) {
+        PageResult<ProductDO> pageResult = productService.getProductPage(pageVO);
+        return success(ProductConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出产品 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:product:export')")
+    @OperateLog(type = EXPORT)
+    public void exportProductExcel(@Valid ProductExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<ProductDO> list = productService.getProductList(exportReqVO);
+        // 导出 Excel
+        List<ProductExcelVO> datas = ProductConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "产品.xls", "数据", ProductExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductBaseVO.java
new file mode 100644
index 000000000..e6ba132f7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductBaseVO.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import javax.validation.constraints.*;
+
+/**
+ * 产品 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ProductBaseVO {
+
+    @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotNull(message = "产品名称不能为空")
+    private String name;
+
+    @Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "产品编码不能为空")
+    private String no;
+
+    @Schema(description = "单位")
+    private String unit;
+
+    @Schema(description = "价格", example = "8911")
+    private Long price;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "状态不能为空")
+    private Integer status;
+
+    @Schema(description = "产品分类ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1738")
+    @NotNull(message = "产品分类ID不能为空")
+    private Long categoryId;
+
+    @Schema(description = "产品描述", example = "你说的对")
+    private String description;
+
+    @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31926")
+    @NotNull(message = "负责人的用户编号不能为空")
+    private Long ownerUserId;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductCreateReqVO.java
new file mode 100644
index 000000000..90e36d031
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 产品创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ProductCreateReqVO extends ProductBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExcelVO.java
new file mode 100644
index 000000000..0e2178f63
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExcelVO.java
@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+
+/**
+ * 产品 Excel VO
+ *
+ * @author ZanGe丶
+ */
+@Data
+public class ProductExcelVO {
+
+    @ExcelProperty("主键id")
+    private Long id;
+
+    @ExcelProperty("产品名称")
+    private String name;
+
+    @ExcelProperty("产品编码")
+    private String no;
+
+    @ExcelProperty("单位")
+    private String unit;
+
+    @ExcelProperty("价格")
+    private Long price;
+
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat("crm_product_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Integer status;
+
+    @ExcelProperty("产品分类ID")
+    private Long categoryId;
+
+    @ExcelProperty("产品描述")
+    private String description;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExportReqVO.java
new file mode 100644
index 000000000..9c8aa4051
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExportReqVO.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import java.time.LocalDateTime;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 产品 Excel 导出 Request VO,参数和 ProductPageReqVO 是一致的")
+@Data
+public class ProductExportReqVO {
+
+    @Schema(description = "产品名称", example = "李四")
+    private String name;
+
+    @Schema(description = "产品编码")
+    private String no;
+
+    @Schema(description = "单位")
+    private String unit;
+
+    @Schema(description = "价格", example = "8911")
+    private Long price;
+
+    @Schema(description = "状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "产品分类ID", example = "1738")
+    private Long categoryId;
+
+    @Schema(description = "产品描述", example = "你说的对")
+    private String description;
+
+    @Schema(description = "负责人的用户编号", example = "31926")
+    private Long ownerUserId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductPageReqVO.java
new file mode 100644
index 000000000..46285b9e3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductPageReqVO.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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 ProductPageReqVO extends PageParam {
+
+    @Schema(description = "产品名称", example = "李四")
+    private String name;
+
+    @Schema(description = "产品编码")
+    private String no;
+
+    @Schema(description = "单位")
+    private String unit;
+
+    @Schema(description = "价格", example = "8911")
+    private Long price;
+
+    @Schema(description = "状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "产品分类ID", example = "1738")
+    private Long categoryId;
+
+    @Schema(description = "产品描述", example = "你说的对")
+    private String description;
+
+    @Schema(description = "负责人的用户编号", example = "31926")
+    private Long ownerUserId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductRespVO.java
new file mode 100644
index 000000000..b5a3c468a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 产品 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ProductRespVO extends ProductBaseVO {
+
+    @Schema(description = "主键id", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductUpdateReqVO.java
new file mode 100644
index 000000000..7ecf2e485
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 产品更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ProductUpdateReqVO extends ProductBaseVO {
+
+    @Schema(description = "主键id", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+    @NotNull(message = "主键id不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/ProductCategoryController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/ProductCategoryController.java
new file mode 100644
index 000000000..38b85c512
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/ProductCategoryController.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.crm.controller.admin.productcategory;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.productcategory.ProductCategoryDO;
+import cn.iocoder.yudao.module.crm.convert.productcategory.ProductCategoryConvert;
+import cn.iocoder.yudao.module.crm.service.productcategory.ProductCategoryService;
+
+@Tag(name = "管理后台 - 产品分类")
+@RestController
+@RequestMapping("/crm/product-category")
+@Validated
+public class ProductCategoryController {
+
+    @Resource
+    private ProductCategoryService productCategoryService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建产品分类")
+    @PreAuthorize("@ss.hasPermission('crm:product-category:create')")
+    public CommonResult<Long> createProductCategory(@Valid @RequestBody ProductCategoryCreateReqVO createReqVO) {
+        return success(productCategoryService.createProductCategory(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新产品分类")
+    @PreAuthorize("@ss.hasPermission('crm:product-category:update')")
+    public CommonResult<Boolean> updateProductCategory(@Valid @RequestBody ProductCategoryUpdateReqVO updateReqVO) {
+        productCategoryService.updateProductCategory(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除产品分类")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:product-category:delete')")
+    public CommonResult<Boolean> deleteProductCategory(@RequestParam("id") Long id) {
+        productCategoryService.deleteProductCategory(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得产品分类")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:product-category:query')")
+    public CommonResult<ProductCategoryRespVO> getProductCategory(@RequestParam("id") Long id) {
+        ProductCategoryDO productCategory = productCategoryService.getProductCategory(id);
+        return success(ProductCategoryConvert.INSTANCE.convert(productCategory));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得产品分类列表")
+    @PreAuthorize("@ss.hasPermission('crm:product-category:query')")
+    public CommonResult<List<ProductCategoryRespVO>> getProductCategoryList(@Valid ProductCategoryListReqVO treeListReqVO) {
+        List<ProductCategoryDO> list = productCategoryService.getProductCategoryList(treeListReqVO);
+        return success(ProductCategoryConvert.INSTANCE.convertList(list));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryBaseVO.java
new file mode 100644
index 000000000..e120a4567
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryBaseVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import javax.validation.constraints.*;
+
+/**
+ * 产品分类 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ProductCategoryBaseVO {
+
+    @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
+    @NotNull(message = "名称不能为空")
+    private String name;
+
+    @Schema(description = "父级id", requiredMode = Schema.RequiredMode.REQUIRED, example = "4680")
+    @NotNull(message = "父级id不能为空")
+    private Long parentId;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryCreateReqVO.java
new file mode 100644
index 000000000..87f2096bf
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 产品分类创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ProductCategoryCreateReqVO extends ProductCategoryBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryListReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryListReqVO.java
new file mode 100644
index 000000000..c29c72148
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryListReqVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 产品分类 List VO
+ *
+ * @author ZanGe丶
+ */
+@Schema(description = "管理后台 - 产品分类列表 Request VO")
+@Data
+public class ProductCategoryListReqVO {
+
+    @ExcelProperty("名称")
+    private String name;
+
+    @ExcelProperty("父级id")
+    private Long parentId;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryRespVO.java
new file mode 100644
index 000000000..a24fc9c4d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 产品分类 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ProductCategoryRespVO extends ProductCategoryBaseVO {
+
+    @Schema(description = "主键id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23902")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryUpdateReqVO.java
new file mode 100644
index 000000000..48d7bf4f2
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 产品分类更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ProductCategoryUpdateReqVO extends ProductCategoryBaseVO {
+
+    @Schema(description = "主键id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23902")
+    @NotNull(message = "主键id不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/product/ProductConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/product/ProductConvert.java
new file mode 100644
index 000000000..adc389f2e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/product/ProductConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.convert.product;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.product.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.product.ProductDO;
+
+/**
+ * 产品 Convert
+ *
+ * @author ZanGe丶
+ */
+@Mapper
+public interface ProductConvert {
+
+    ProductConvert INSTANCE = Mappers.getMapper(ProductConvert.class);
+
+    ProductDO convert(ProductCreateReqVO bean);
+
+    ProductDO convert(ProductUpdateReqVO bean);
+
+    ProductRespVO convert(ProductDO bean);
+
+    List<ProductRespVO> convertList(List<ProductDO> list);
+
+    PageResult<ProductRespVO> convertPage(PageResult<ProductDO> page);
+
+    List<ProductExcelVO> convertList02(List<ProductDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/productcategory/ProductCategoryConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/productcategory/ProductCategoryConvert.java
new file mode 100644
index 000000000..ebc4f36e3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/productcategory/ProductCategoryConvert.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.crm.convert.productcategory;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.productcategory.ProductCategoryDO;
+
+/**
+ * 产品分类 Convert
+ *
+ * @author ZanGe丶
+ */
+@Mapper
+public interface ProductCategoryConvert {
+
+    ProductCategoryConvert INSTANCE = Mappers.getMapper(ProductCategoryConvert.class);
+
+    ProductCategoryDO convert(ProductCategoryCreateReqVO bean);
+
+    ProductCategoryDO convert(ProductCategoryUpdateReqVO bean);
+
+    ProductCategoryRespVO convert(ProductCategoryDO bean);
+
+    List<ProductCategoryRespVO> convertList(List<ProductCategoryDO> list);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/ProductDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/ProductDO.java
new file mode 100644
index 000000000..c7b8ec009
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/ProductDO.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.product;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 产品 DO
+ *
+ * @author ZanGe丶
+ */
+@TableName("crm_product")
+@KeySequence("crm_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductDO extends BaseDO {
+
+    /**
+     * 主键id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 产品名称
+     */
+    private String name;
+    /**
+     * 产品编码
+     */
+    private String no;
+    /**
+     * 单位
+     */
+    private String unit;
+    /**
+     * 价格
+     */
+    private Long price;
+    /**
+     * 状态
+     *
+     * 枚举 {@link TODO crm_product_status 对应的类}
+     */
+    private Integer status;
+    /**
+     * 产品分类ID
+     */
+    private Long categoryId;
+    /**
+     * 产品描述
+     */
+    private String description;
+    /**
+     * 负责人的用户编号
+     */
+    private Long ownerUserId;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/productcategory/ProductCategoryDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/productcategory/ProductCategoryDO.java
new file mode 100644
index 000000000..9e968ac86
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/productcategory/ProductCategoryDO.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.productcategory;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 产品分类 DO
+ *
+ * @author ZanGe丶
+ */
+@TableName("crm_product_category")
+@KeySequence("crm_product_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductCategoryDO extends BaseDO {
+
+    /**
+     * 主键id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名称
+     */
+    private String name;
+    /**
+     * 父级id
+     */
+    private Long parentId;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/product/ProductMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/product/ProductMapper.java
new file mode 100644
index 000000000..bf5e6b993
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/product/ProductMapper.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.product;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.product.ProductDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.product.vo.*;
+
+/**
+ * 产品 Mapper
+ *
+ * @author ZanGe丶
+ */
+@Mapper
+public interface ProductMapper extends BaseMapperX<ProductDO> {
+
+    default PageResult<ProductDO> selectPage(ProductPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ProductDO>()
+                .likeIfPresent(ProductDO::getName, reqVO.getName())
+                .likeIfPresent(ProductDO::getNo, reqVO.getNo())
+                .eqIfPresent(ProductDO::getUnit, reqVO.getUnit())
+                .eqIfPresent(ProductDO::getPrice, reqVO.getPrice())
+                .eqIfPresent(ProductDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(ProductDO::getCategoryId, reqVO.getCategoryId())
+                .eqIfPresent(ProductDO::getDescription, reqVO.getDescription())
+                .eqIfPresent(ProductDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(ProductDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ProductDO::getId));
+    }
+
+    default List<ProductDO> selectList(ProductExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<ProductDO>()
+                .likeIfPresent(ProductDO::getName, reqVO.getName())
+                .likeIfPresent(ProductDO::getNo, reqVO.getNo())
+                .eqIfPresent(ProductDO::getUnit, reqVO.getUnit())
+                .eqIfPresent(ProductDO::getPrice, reqVO.getPrice())
+                .eqIfPresent(ProductDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(ProductDO::getCategoryId, reqVO.getCategoryId())
+                .eqIfPresent(ProductDO::getDescription, reqVO.getDescription())
+                .eqIfPresent(ProductDO::getOwnerUserId, reqVO.getOwnerUserId())
+                .betweenIfPresent(ProductDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ProductDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/productcategory/ProductCategoryMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/productcategory/ProductCategoryMapper.java
new file mode 100644
index 000000000..a7a59e032
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/productcategory/ProductCategoryMapper.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.productcategory;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.productcategory.ProductCategoryDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.*;
+
+/**
+ * 产品分类 Mapper
+ *
+ * @author ZanGe丶
+ */
+@Mapper
+public interface ProductCategoryMapper extends BaseMapperX<ProductCategoryDO> {
+
+
+    default List<ProductCategoryDO> selectList(ProductCategoryListReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<ProductCategoryDO>()
+                .likeIfPresent(ProductCategoryDO::getName, reqVO.getName())
+                .eqIfPresent(ProductCategoryDO::getParentId, reqVO.getParentId())
+                .orderByDesc(ProductCategoryDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductService.java
new file mode 100644
index 000000000..54a1c8639
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductService.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.crm.service.product;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.crm.controller.admin.product.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.product.ProductDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 产品 Service 接口
+ *
+ * @author ZanGe丶
+ */
+public interface ProductService {
+
+    /**
+     * 创建产品
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createProduct(@Valid ProductCreateReqVO createReqVO);
+
+    /**
+     * 更新产品
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateProduct(@Valid ProductUpdateReqVO updateReqVO);
+
+    /**
+     * 删除产品
+     *
+     * @param id 编号
+     */
+    void deleteProduct(Long id);
+
+    /**
+     * 获得产品
+     *
+     * @param id 编号
+     * @return 产品
+     */
+    ProductDO getProduct(Long id);
+
+    /**
+     * 获得产品列表
+     *
+     * @param ids 编号
+     * @return 产品列表
+     */
+    List<ProductDO> getProductList(Collection<Long> ids);
+
+    /**
+     * 获得产品分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 产品分页
+     */
+    PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO);
+
+    /**
+     * 获得产品列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 产品列表
+     */
+    List<ProductDO> getProductList(ProductExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductServiceImpl.java
new file mode 100644
index 000000000..82294de85
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductServiceImpl.java
@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.module.crm.service.product;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.product.vo.ProductCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.product.vo.ProductExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.product.vo.ProductPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.product.vo.ProductUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.product.ProductConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.product.ProductDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.product.ProductMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.PRODUCT_NO_EXISTS;
+
+/**
+ * 产品 Service 实现类
+ *
+ * @author ZanGe丶
+ */
+@Service
+@Validated
+public class ProductServiceImpl implements ProductService {
+
+    @Resource
+    private ProductMapper productMapper;
+
+    @Override
+    public Long createProduct(ProductCreateReqVO createReqVO) {
+        //校验产品编号是否存在
+        validateProductNo(createReqVO.getNo());
+        // 插入
+        ProductDO product = ProductConvert.INSTANCE.convert(createReqVO);
+        productMapper.insert(product);
+        // 返回
+        return product.getId();
+    }
+
+    @Override
+    public void updateProduct(ProductUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateProductExists(updateReqVO.getId(), updateReqVO.getNo());
+        // 更新
+        ProductDO updateObj = ProductConvert.INSTANCE.convert(updateReqVO);
+        productMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteProduct(Long id) {
+        // 校验存在
+        validateProductExists(id, null);
+        // 删除
+        productMapper.deleteById(id);
+    }
+
+    private void validateProductExists(Long id, String no) {
+        ProductDO product = productMapper.selectById(id);
+        if (product == null) {
+            throw exception(PRODUCT_NOT_EXISTS);
+        }
+        if (no != null && no.equals(product.getNo())) {
+            throw exception(PRODUCT_NO_EXISTS);
+        }
+    }
+
+    @Override
+    public ProductDO getProduct(Long id) {
+        return productMapper.selectById(id);
+    }
+
+    @Override
+    public List<ProductDO> getProductList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return productMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO) {
+        return productMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<ProductDO> getProductList(ProductExportReqVO exportReqVO) {
+        return productMapper.selectList(exportReqVO);
+    }
+
+    private void validateProductNo(String no) {
+        ProductDO product = productMapper.selectOne(new LambdaQueryWrapper<ProductDO>().eq(ProductDO::getNo, no));
+        if (product != null) {
+            throw exception(PRODUCT_NO_EXISTS);
+        }
+    }
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryService.java
new file mode 100644
index 000000000..7c64ee02b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryService.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.crm.service.productcategory;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.productcategory.ProductCategoryDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 产品分类 Service 接口
+ *
+ * @author ZanGe丶
+ */
+public interface ProductCategoryService {
+
+    /**
+     * 创建产品分类
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createProductCategory(@Valid ProductCategoryCreateReqVO createReqVO);
+
+    /**
+     * 更新产品分类
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateProductCategory(@Valid ProductCategoryUpdateReqVO updateReqVO);
+
+    /**
+     * 删除产品分类
+     *
+     * @param id 编号
+     */
+    void deleteProductCategory(Long id);
+
+    /**
+     * 获得产品分类
+     *
+     * @param id 编号
+     * @return 产品分类
+     */
+    ProductCategoryDO getProductCategory(Long id);
+
+    /**
+     * 获得产品分类列表
+     *
+     * @param ids 编号
+     * @return 产品分类列表
+     */
+    List<ProductCategoryDO> getProductCategoryList(ProductCategoryListReqVO treeListReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryServiceImpl.java
new file mode 100644
index 000000000..7d3d6fa86
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryServiceImpl.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.crm.service.productcategory;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.productcategory.ProductCategoryDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.convert.productcategory.ProductCategoryConvert;
+import cn.iocoder.yudao.module.crm.dal.mysql.productcategory.ProductCategoryMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+
+/**
+ * 产品分类 Service 实现类
+ *
+ * @author ZanGe丶
+ */
+@Service
+@Validated
+public class ProductCategoryServiceImpl implements ProductCategoryService {
+
+    @Resource
+    private ProductCategoryMapper productCategoryMapper;
+
+    @Override
+    public Long createProductCategory(ProductCategoryCreateReqVO createReqVO) {
+        // 插入
+        ProductCategoryDO productCategory = ProductCategoryConvert.INSTANCE.convert(createReqVO);
+        productCategoryMapper.insert(productCategory);
+        // 返回
+        return productCategory.getId();
+    }
+
+    @Override
+    public void updateProductCategory(ProductCategoryUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateProductCategoryExists(updateReqVO.getId());
+        // 更新
+        ProductCategoryDO updateObj = ProductCategoryConvert.INSTANCE.convert(updateReqVO);
+        productCategoryMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteProductCategory(Long id) {
+        // 校验存在
+        validateProductCategoryExists(id);
+        // 删除
+        productCategoryMapper.deleteById(id);
+    }
+
+    private void validateProductCategoryExists(Long id) {
+        if (productCategoryMapper.selectById(id) == null) {
+            throw exception(PRODUCT_CATEGORY_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ProductCategoryDO getProductCategory(Long id) {
+        return productCategoryMapper.selectById(id);
+    }
+
+    @Override
+    public List<ProductCategoryDO> getProductCategoryList(ProductCategoryListReqVO treeListReqVO) {
+        return productCategoryMapper.selectList(treeListReqVO);
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/product/ProductMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/product/ProductMapper.xml
new file mode 100644
index 000000000..f1a52d2f1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/product/ProductMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.product.ProductMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/productcategory/ProductCategoryMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/productcategory/ProductCategoryMapper.xml
new file mode 100644
index 000000000..f3b4d0d4a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/productcategory/ProductCategoryMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.productcategory.ProductCategoryMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

From 989d7c44d0b4c60e64bb49e59132b471f294fc99 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 4 Nov 2023 22:52:41 +0800
Subject: [PATCH 065/101] =?UTF-8?q?code=20review=20=E5=BA=97=E9=93=BA?=
 =?UTF-8?q?=E8=A3=85=E4=BF=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pom.xml                                       |  6 +-
 .../diy/vo/template/DiyTemplateBaseVO.java    |  4 +-
 .../dal/dataobject/diy/DiyPageDO.java         |  2 +
 .../dal/dataobject/diy/DiyTemplateDO.java     |  3 +
 .../service/diy/DiyPageServiceImpl.java       |  8 ++-
 .../service/diy/DiyTemplateServiceImpl.java   |  7 ++-
 yudao-server/pom.xml                          | 62 +++++++++----------
 7 files changed, 54 insertions(+), 38 deletions(-)

diff --git a/pom.xml b/pom.xml
index dae40f3dc..7f7c140bb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,12 +15,12 @@
         <!-- 各种 module 拓展 -->
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
-<!--        <module>yudao-module-member</module>-->
+        <module>yudao-module-member</module>
 <!--        <module>yudao-module-bpm</module>-->
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-mp</module>-->
-<!--        <module>yudao-module-pay</module>-->
-<!--        <module>yudao-module-mall</module>-->
+        <module>yudao-module-pay</module>
+        <module>yudao-module-mall</module>
         <!-- 示例项目 -->
 <!--        <module>yudao-example</module>-->
     </modules>
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java
index 7959b6c26..b79c7231f 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/diy/vo/template/DiyTemplateBaseVO.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.promotion.controller.admin.diy.vo.template;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
-import javax.validation.constraints.NotNull;
+import javax.validation.constraints.NotEmpty;
 import java.util.List;
 
 /**
@@ -14,7 +14,7 @@ import java.util.List;
 public class DiyTemplateBaseVO {
 
     @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "默认主题")
-    @NotNull(message = "模板名称不能为空")
+    @NotEmpty(message = "模板名称不能为空")
     private String name;
 
     @Schema(description = "备注", example = "默认主题")
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java
index e5e3f4208..7e1044104 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyPageDO.java
@@ -32,6 +32,8 @@ public class DiyPageDO extends BaseDO {
     private Long id;
     /**
      * 装修模板编号
+     *
+     * 关联 {@link DiyTemplateDO#getId()}
      */
     private Long templateId;
     /**
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java
index a50fd4dde..684a6f9cb 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/diy/DiyTemplateDO.java
@@ -14,6 +14,9 @@ import java.util.List;
 /**
  * 装修模板 DO
  *
+ * 1. 新建一个模版,下面可以包含多个 {@link DiyPageDO} 页面,例如说首页、我的
+ * 2. 如果需要使用某个模版,则将 {@link #used} 设置为 true,表示已使用,有且仅有一个
+ *
  * @author owen
  */
 @TableName(value = "promotion_diy_template", autoResultMap = true)
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java
index d82b9b8ed..69f96ad8f 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyPageServiceImpl.java
@@ -42,7 +42,6 @@ public class DiyPageServiceImpl implements DiyPageService {
         DiyPageDO diyPage = DiyPageConvert.INSTANCE.convert(createReqVO);
         diyPage.setProperty("{}");
         diyPageMapper.insert(diyPage);
-        // 返回
         return diyPage.getId();
     }
 
@@ -57,6 +56,13 @@ public class DiyPageServiceImpl implements DiyPageService {
         diyPageMapper.updateById(updateObj);
     }
 
+    /**
+     * 校验 Page 页面,在一个 template 模版下的名字是唯一的
+     *
+     * @param id Page 编号
+     * @param templateId 模版编号
+     * @param name Page 名字
+     */
     void validateNameUnique(Long id, Long templateId, String name) {
         if (templateId != null || StrUtil.isBlank(name)) {
             return;
diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java
index 41025c5d6..530b31b94 100644
--- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/diy/DiyTemplateServiceImpl.java
@@ -32,9 +32,11 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
 
     @Resource
     private DiyTemplateMapper diyTemplateMapper;
+
     @Resource
     private DiyPageService diyPageService;
 
+    // TODO @疯狂:事务;
     @Override
     public Long createDiyTemplate(DiyTemplateCreateReqVO createReqVO) {
         // 校验名称唯一
@@ -120,9 +122,11 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
     }
 
     @Override
+    // TODO @疯狂:事务;
     public void useDiyTemplate(Long id) {
         // 校验存在
         validateDiyTemplateExists(id);
+        // TODO @疯狂:要不已使用的情况,抛个业务异常?
         // 已使用的更新为未使用
         DiyTemplateDO used = diyTemplateMapper.selectByUsed(true);
         if (used != null) {
@@ -136,8 +140,8 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
         this.updateUsed(id, true, LocalDateTime.now());
     }
 
-    @Transactional(rollbackFor = Exception.class)
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void updateDiyTemplateProperty(DiyTemplatePropertyUpdateRequestVO updateReqVO) {
         // 校验存在
         validateDiyTemplateExists(updateReqVO.getId());
@@ -151,6 +155,7 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
         return diyTemplateMapper.selectByUsed(true);
     }
 
+    // TODO @疯狂:挪到 useDiyTemplate 下面,改名 updateTemplateUsed 会不会好点哈;
     /**
      * 更新模板是否使用
      *
diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index 87a961778..4463bd7fb 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -37,11 +37,11 @@
         </dependency>
 
         <!-- 会员中心。默认注释,保证编译速度 -->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-member-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-member-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
         <!-- 数据报表。默认注释,保证编译速度 -->
 <!--        <dependency>-->
@@ -56,11 +56,11 @@
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
         <!-- 支付服务。默认注释,保证编译速度 -->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-pay-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-pay-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
         <!-- 微信公众号模块。默认注释,保证编译速度 -->
 <!--        <dependency>-->
@@ -69,27 +69,27 @@
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
 
-<!--         商城相关模块。默认注释,保证编译速度-->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-promotion-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-product-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-trade-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-statistics-biz</artifactId>-->
-<!--            <version>${revision}</version>-->
-<!--        </dependency>-->
+        <!-- 商城相关模块。默认注释,保证编译速度-->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-promotion-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-product-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-trade-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-statistics-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
 
         <!-- spring boot 配置所需依赖 -->
         <dependency>

From ca184026cfcde8bb6a5e3cfeeeb49ddd4c00b851 Mon Sep 17 00:00:00 2001
From: kehaiyou <71740796@qq.com>
Date: Sun, 5 Nov 2023 14:21:37 +0800
Subject: [PATCH 066/101] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E6=B5=81?=
 =?UTF-8?q?=E7=A8=8B=E5=AE=9E=E4=BE=8B=E6=97=B6=EF=BC=8C=E6=8F=90=E5=89=8D?=
 =?UTF-8?q?=E6=8C=87=E6=B4=BE=E5=AE=A1=E6=89=B9=E4=BA=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/context/FlowableContextHolder.java   | 39 +++++++++++++++++++
 .../dto/BpmProcessInstanceCreateReqDTO.java   |  7 ++++
 .../BpmProcessInstanceCreateReqVO.java        |  4 ++
 .../task/BpmProcessInstanceExtDO.java         |  6 +++
 .../BpmTaskAssignRuleServiceImpl.java         | 11 ++++++
 .../task/BpmProcessInstanceService.java       | 19 ++++++---
 .../task/BpmProcessInstanceServiceImpl.java   |  2 +-
 7 files changed, 82 insertions(+), 6 deletions(-)
 create mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java

diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
new file mode 100644
index 000000000..e6021c0b7
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.framework.flowable.core.context;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工作流--用户用到的上下文相关信息
+ */
+public class FlowableContextHolder {
+
+    private static final ThreadLocal<Map<String, List<Long>>> ASSIGNEE = new TransmittableThreadLocal<>();
+
+    /**
+     * 通过流程任务的定义 key ,拿到提前选好的审批人
+     * 此方法目的:首次创建流程实例时,数据库中还查询不到 assignee 字段,所以存入上下文中获取
+     *
+     * @param taskDefinitionKey 流程任务 key
+     * @return 审批人 ID 集合
+     */
+    public static List<Long> getAssigneeByTaskDefinitionKey(String taskDefinitionKey) {
+        if (CollUtil.isNotEmpty(ASSIGNEE.get())) {
+            return ASSIGNEE.get().get(taskDefinitionKey);
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * 存入提前选好的审批人到上下文线程变量中
+     *
+     * @param assignee 流程任务 key -> 审批人 ID 炅和
+     */
+    public static void setAssignee(Map<String, List<Long>> assignee) {
+        ASSIGNEE.set(assignee);
+    }
+}
diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
index 5d7edbe80..6db999c1e 100644
--- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
+++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.api.task.dto;
 import lombok.Data;
 
 import javax.validation.constraints.NotEmpty;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -30,4 +31,10 @@ public class BpmProcessInstanceCreateReqDTO {
      */
     @NotEmpty(message = "业务的唯一标识")
     private String businessKey;
+
+    /**
+     * 提前指派的审批人
+     * 例如: { taskKey1 :[1,2] },则表示 taskKey1 这个任务,提前设定了,由 userId 为 1,2 的用户进行审批
+     */
+    private Map<String, List<Long>> assignee;
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
index 8fc544ef4..13b7c8f0b 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import javax.validation.constraints.NotEmpty;
+import java.util.List;
 import java.util.Map;
 
 @Schema(description = "管理后台 - 流程实例的创建 Request VO")
@@ -17,4 +18,7 @@ public class BpmProcessInstanceCreateReqVO {
     @Schema(description = "变量实例")
     private Map<String, Object> variables;
 
+    @Schema(description = "提前指派的审批人", requiredMode = Schema.RequiredMode.REQUIRED, example = "{taskKey1:[1,2]}")
+    private Map<String, List<Long>> assignee;
+
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
index 293cc2dd7..8aba9059f 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
@@ -12,6 +12,7 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 import java.time.LocalDateTime;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -87,4 +88,9 @@ public class BpmProcessInstanceExtDO extends BaseDO {
     @TableField(typeHandler = JacksonTypeHandler.class)
     private Map<String, Object> formVariables;
 
+    /**
+     * 提前设定好的审批人
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private Map<String, List<Long>> assignee;
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
index c6200b2dd..8c6ceaf48 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
@@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmTaskAssignRuleMapper;
 import cn.iocoder.yudao.module.bpm.enums.DictTypeConstants;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.PostApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -39,6 +40,7 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import javax.validation.Valid;
 import java.util.*;
+import java.util.function.Function;
 
 import static cn.hutool.core.text.CharSequenceUtil.format;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -77,6 +79,9 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
     private DictDataApi dictDataApi;
     @Resource
     private PermissionApi permissionApi;
+    @Resource
+    @Lazy // 解决循环依赖
+    private BpmProcessInstanceService processInstanceService;
     /**
      * 任务分配脚本
      */
@@ -234,6 +239,12 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
     @Override
     @DataPermission(enable = false) // 忽略数据权限,不然分配会存在问题
     public Set<Long> calculateTaskCandidateUsers(DelegateExecution execution) {
+        //1. 先从提前选好的审批人中获取
+        List<Long> assignee = processInstanceService.getAssigneeByProcessInstanceIdAndTaskDefinitionKey(execution.getProcessInstanceId(), execution.getCurrentActivityId());
+        if(CollUtil.isNotEmpty(assignee)){
+            return convertSet(assignee, Function.identity());
+        }
+        //2. 通过分配规则,计算审批人
         BpmTaskAssignRuleDO rule = getTaskRule(execution);
         return calculateTaskCandidateUsers(execution, rule);
     }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
index 23340ad19..4649475db 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
@@ -49,16 +49,17 @@ public interface BpmProcessInstanceService {
     /**
      * 获得流程实例的分页
      *
-     * @param userId 用户编号
+     * @param userId    用户编号
      * @param pageReqVO 分页请求
      * @return 流程实例的分页
      */
     PageResult<BpmProcessInstancePageItemRespVO> getMyProcessInstancePage(Long userId,
                                                                           @Valid BpmProcessInstanceMyPageReqVO pageReqVO);
+
     /**
      * 创建流程实例(提供给前端)
      *
-     * @param userId 用户编号
+     * @param userId      用户编号
      * @param createReqVO 创建信息
      * @return 实例的编号
      */
@@ -67,7 +68,7 @@ public interface BpmProcessInstanceService {
     /**
      * 创建流程实例(提供给内部)
      *
-     * @param userId 用户编号
+     * @param userId       用户编号
      * @param createReqDTO 创建信息
      * @return 实例的编号
      */
@@ -84,7 +85,7 @@ public interface BpmProcessInstanceService {
     /**
      * 取消流程实例
      *
-     * @param userId 用户编号
+     * @param userId      用户编号
      * @param cancelReqVO 取消信息
      */
     void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO);
@@ -139,9 +140,17 @@ public interface BpmProcessInstanceService {
     /**
      * 更新 ProcessInstance 拓展记录为不通过
      *
-     * @param id 流程编号
+     * @param id     流程编号
      * @param reason 理由。例如说,审批不通过时,需要传递该值
      */
     void updateProcessInstanceExtReject(String id, String reason);
 
+    /**
+     * 去流程实例扩展表中,取出指定流程任务提前指定的审批人
+     *
+     * @param processInstanceId 流程实例的编号
+     * @param taskDefinitionKey 流程任务定义的 key
+     * @return 审批人集合
+     */
+    List<Long> getAssigneeByProcessInstanceIdAndTaskDefinitionKey(String processInstanceId, String taskDefinitionKey);
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
index 56115d79b..5d5e1ba10 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -1 +1 @@
-package cn.iocoder.yudao.module.bpm.service.task;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.event.FlowableCancelledEvent;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.*;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;

/**
 * 流程实例 Service 实现类
 *
 * ProcessDefinition & ProcessInstance & Execution & Task 的关系:
 *     1. <a href="https://blog.csdn.net/bobozai86/article/details/105210414" />
 *
 * HistoricProcessInstance & ProcessInstance 的关系:
 *     1. <a href=" https://my.oschina.net/843294669/blog/71902" />
 *
 * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
 *
 * @author 芋道源码
 */
@Service
@Validated
@Slf4j
public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService {

    @Resource
    private RuntimeService runtimeService;
    @Resource
    private BpmProcessInstanceExtMapper processInstanceExtMapper;
    @Resource
    @Lazy // 解决循环依赖
    private BpmTaskService taskService;
    @Resource
    private BpmProcessDefinitionService processDefinitionService;
    @Resource
    private HistoryService historyService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher;
    @Resource
    private BpmMessageService messageService;
    @Override
    public ProcessInstance getProcessInstance(String id) {
        return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
    }

    @Override
    public List<ProcessInstance> getProcessInstances(Set<String> ids) {
        return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
    }

    @Override
    public PageResult<BpmProcessInstancePageItemRespVO> getMyProcessInstancePage(Long userId,
                                                                                 BpmProcessInstanceMyPageReqVO pageReqVO) {
        // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页
        PageResult<BpmProcessInstanceExtDO> pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return new PageResult<>(pageResult.getTotal());
        }

        // 获得流程 Task Map
        List<String> processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId);
        Map<String, List<Task>> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds);
        // 转换返回
        return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId());
        // 发起流程
        return createProcessInstance0(userId, definition, createReqVO.getVariables(), null);
    }

    @Override
    public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey());
        // 发起流程
        return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey());
    }

    @Override
    public BpmProcessInstanceRespVO getProcessInstanceVO(String id) {
        // 获得流程实例
        HistoricProcessInstance processInstance = getHistoricProcessInstance(id);
        if (processInstance == null) {
            return null;
        }
        BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id);
        Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id);

        // 获得流程定义
        ProcessDefinition processDefinition = processDefinitionService
                .getProcessDefinition(processInstance.getProcessDefinitionId());
        Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId());
        BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt(
                processInstance.getProcessDefinitionId());
        Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id);
        String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId());

        // 获得 User
        AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId()));
        DeptRespDTO dept = null;
        if (startUser != null) {
            dept = deptApi.getDept(startUser.getDeptId());
        }

        // 拼接结果
        return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt,
                processDefinition, processDefinitionExt, bpmnXml, startUser, dept);
    }

    @Override
    public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) {
        // 校验流程实例存在
        ProcessInstance instance = getProcessInstance(cancelReqVO.getId());
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
        }
        // 只能取消自己的
        if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
        }

        // 通过删除流程实例,实现流程实例的取消,
        // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询
        deleteProcessInstance(cancelReqVO.getId(),
                BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason()));
    }

    /**
     * 获得历史的流程实例
     *
     * @param id 流程实例的编号
     * @return 历史的流程实例
     */
    @Override
    public HistoricProcessInstance getHistoricProcessInstance(String id) {
        return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult();
    }

    @Override
    public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) {
        return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
    }

    @Override
    public void createProcessInstanceExt(ProcessInstance instance) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId());
        // 插入 BpmProcessInstanceExtDO 对象
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(instance.getId())
                .setProcessDefinitionId(definition.getId())
                .setName(instance.getProcessDefinitionName())
                .setStartUserId(Long.valueOf(instance.getStartUserId()))
                .setCategory(definition.getCategory())
                .setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus())
                .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult());

        processInstanceExtMapper.insert(instanceExtDO);
    }

    @Override
    public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) {
        // 判断是否为 Reject 不通过。如果是,则不进行更新.
        // 因为,updateProcessInstanceExtReject 方法,已经进行更新了
        if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String)event.getCause())) {
            return;
        }

        // 需要主动查询,因为 instance 只有 id 属性
        // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
        HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId());
        // 更新拓展表
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(event.getProcessInstanceId())
                .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.CANCEL.getResult());
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    @Override
    public void updateProcessInstanceExtComplete(ProcessInstance instance) {
        // 需要主动查询,因为 instance 只有 id 属性
        // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
        HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId());
        // 更新拓展表
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(instance.getProcessInstanceId())
                .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程被通过的消息
        messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance));

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProcessInstanceExtReject(String id, String reason) {
        // 需要主动查询,因为 instance 只有 id 属性
        ProcessInstance processInstance = getProcessInstance(id);
        // 删除流程实例,以实现驳回任务时,取消整个审批流程
        deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason)));

        // 更新 status + result
        // 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法,
        // 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id)
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.REJECT.getResult());
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程被不通过的消息
        messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason));

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    private void deleteProcessInstance(String id, String reason) {
        runtimeService.deleteProcessInstance(id, reason);
    }

    private String createProcessInstance0(Long userId, ProcessDefinition definition,
                                          Map<String, Object> variables, String businessKey) {
        // 校验流程定义
        if (definition == null) {
            throw exception(PROCESS_DEFINITION_NOT_EXISTS);
        }
        if (definition.isSuspended()) {
            throw exception(PROCESS_DEFINITION_IS_SUSPENDED);
        }

        // 创建流程实例
        ProcessInstance instance = runtimeService.createProcessInstanceBuilder()
                .processDefinitionId(definition.getId())
                .businessKey(businessKey)
                .name(definition.getName().trim())
                .variables(variables)
                .start();
        // 设置流程名字
        runtimeService.setProcessInstanceName(instance.getId(), definition.getName());

        // 补全流程实例的拓展表
        processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId())
                .setFormVariables(variables));
        return instance.getId();
    }

}
\ No newline at end of file
+package cn.iocoder.yudao.module.bpm.service.task;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.flowable.core.context.FlowableContextHolder;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.event.FlowableCancelledEvent;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.*;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;

/**
 * 流程实例 Service 实现类
 * <p>
 * ProcessDefinition & ProcessInstance & Execution & Task 的关系:
 * 1. <a href="https://blog.csdn.net/bobozai86/article/details/105210414" />
 * <p>
 * HistoricProcessInstance & ProcessInstance 的关系:
 * 1. <a href=" https://my.oschina.net/843294669/blog/71902" />
 * <p>
 * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
 *
 * @author 芋道源码
 */
@Service
@Validated
@Slf4j
public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService {

    @Resource
    private RuntimeService runtimeService;
    @Resource
    private BpmProcessInstanceExtMapper processInstanceExtMapper;
    @Resource
    @Lazy // 解决循环依赖
    private BpmTaskService taskService;
    @Resource
    private BpmProcessDefinitionService processDefinitionService;
    @Resource
    private HistoryService historyService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher;
    @Resource
    private BpmMessageService messageService;

    @Override
    public ProcessInstance getProcessInstance(String id) {
        return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
    }

    @Override
    public List<ProcessInstance> getProcessInstances(Set<String> ids) {
        return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
    }

    @Override
    public PageResult<BpmProcessInstancePageItemRespVO> getMyProcessInstancePage(Long userId,
                                                                                 BpmProcessInstanceMyPageReqVO pageReqVO) {
        // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页
        PageResult<BpmProcessInstanceExtDO> pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return new PageResult<>(pageResult.getTotal());
        }

        // 获得流程 Task Map
        List<String> processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId);
        Map<String, List<Task>> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds);
        // 转换返回
        return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId());
        // 发起流程
        return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getAssignee());
    }

    @Override
    public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey());
        // 发起流程
        return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getAssignee());
    }

    @Override
    public BpmProcessInstanceRespVO getProcessInstanceVO(String id) {
        // 获得流程实例
        HistoricProcessInstance processInstance = getHistoricProcessInstance(id);
        if (processInstance == null) {
            return null;
        }
        BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id);
        Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id);

        // 获得流程定义
        ProcessDefinition processDefinition = processDefinitionService
                .getProcessDefinition(processInstance.getProcessDefinitionId());
        Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId());
        BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt(
                processInstance.getProcessDefinitionId());
        Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id);
        String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId());

        // 获得 User
        AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId()));
        DeptRespDTO dept = null;
        if (startUser != null) {
            dept = deptApi.getDept(startUser.getDeptId());
        }

        // 拼接结果
        return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt,
                processDefinition, processDefinitionExt, bpmnXml, startUser, dept);
    }

    @Override
    public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) {
        // 校验流程实例存在
        ProcessInstance instance = getProcessInstance(cancelReqVO.getId());
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
        }
        // 只能取消自己的
        if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
        }

        // 通过删除流程实例,实现流程实例的取消,
        // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询
        deleteProcessInstance(cancelReqVO.getId(),
                BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason()));
    }

    /**
     * 获得历史的流程实例
     *
     * @param id 流程实例的编号
     * @return 历史的流程实例
     */
    @Override
    public HistoricProcessInstance getHistoricProcessInstance(String id) {
        return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult();
    }

    @Override
    public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) {
        return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
    }

    @Override
    public void createProcessInstanceExt(ProcessInstance instance) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId());
        // 插入 BpmProcessInstanceExtDO 对象
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(instance.getId())
                .setProcessDefinitionId(definition.getId())
                .setName(instance.getProcessDefinitionName())
                .setStartUserId(Long.valueOf(instance.getStartUserId()))
                .setCategory(definition.getCategory())
                .setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus())
                .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult());

        processInstanceExtMapper.insert(instanceExtDO);
    }

    @Override
    public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) {
        // 判断是否为 Reject 不通过。如果是,则不进行更新.
        // 因为,updateProcessInstanceExtReject 方法,已经进行更新了
        if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String) event.getCause())) {
            return;
        }

        // 需要主动查询,因为 instance 只有 id 属性
        // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
        HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId());
        // 更新拓展表
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(event.getProcessInstanceId())
                .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.CANCEL.getResult());
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    @Override
    public void updateProcessInstanceExtComplete(ProcessInstance instance) {
        // 需要主动查询,因为 instance 只有 id 属性
        // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
        HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId());
        // 更新拓展表
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(instance.getProcessInstanceId())
                .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程被通过的消息
        messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance));

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProcessInstanceExtReject(String id, String reason) {
        // 需要主动查询,因为 instance 只有 id 属性
        ProcessInstance processInstance = getProcessInstance(id);
        // 删除流程实例,以实现驳回任务时,取消整个审批流程
        deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason)));

        // 更新 status + result
        // 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法,
        // 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id)
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.REJECT.getResult());
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程被不通过的消息
        messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason));

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    private void deleteProcessInstance(String id, String reason) {
        runtimeService.deleteProcessInstance(id, reason);
    }

    private String createProcessInstance0(Long userId, ProcessDefinition definition,
                                          Map<String, Object> variables, String businessKey,
                                          Map<String, List<Long>> assignee) {
        // 校验流程定义
        if (definition == null) {
            throw exception(PROCESS_DEFINITION_NOT_EXISTS);
        }
        if (definition.isSuspended()) {
            throw exception(PROCESS_DEFINITION_IS_SUSPENDED);
        }
        //设置上下文信息
        FlowableContextHolder.setAssignee(assignee);

        // 创建流程实例
        ProcessInstance instance = runtimeService.createProcessInstanceBuilder()
                .processDefinitionId(definition.getId())
                .businessKey(businessKey)
                .name(definition.getName().trim())
                .variables(variables)
                .start();
        // 设置流程名字
        runtimeService.setProcessInstanceName(instance.getId(), definition.getName());

        // 补全流程实例的拓展表
        processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId())
                .setFormVariables(variables).setAssignee(assignee));
        return instance.getId();
    }

    @Override
    public List<Long> getAssigneeByProcessInstanceIdAndTaskDefinitionKey(String processInstanceId, String taskDefinitionKey) {
        //1. 先从上下文获取,首次提交数据库中查询不到
        List<Long> result = FlowableContextHolder.getAssigneeByTaskDefinitionKey(taskDefinitionKey);
        if (CollUtil.isNotEmpty(result)) {
            return result;
        }
        //2. 从数据库中获取
        BpmProcessInstanceExtDO instance = processInstanceExtMapper.selectByProcessInstanceId(processInstanceId);
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
        }
        if(CollUtil.isNotEmpty(instance.getAssignee())){
            return instance.getAssignee().get(taskDefinitionKey);
        }
        return Collections.emptyList();
    }

}
\ No newline at end of file

From 9537669d01cadbd46b4b22218baddfdd6e67cd0a Mon Sep 17 00:00:00 2001
From: kehaiyou <71740796@qq.com>
Date: Sun, 5 Nov 2023 20:14:21 +0800
Subject: [PATCH 067/101] =?UTF-8?q?fix:=20=E5=8E=BB=E9=99=A4=E6=97=A0?=
 =?UTF-8?q?=E7=94=A8=E8=A1=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/cn/iocoder/yudao/framework/common/package-info.java     | 1 -
 1 file changed, 1 deletion(-)

diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java
index 09c43f748..f3f2574e5 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java
@@ -1,7 +1,6 @@
 /**
  * 基础的通用类,和框架无关
  *
- * sdadsdsd
  * 例如说,CommonResult 为通用返回
  */
 package cn.iocoder.yudao.framework.common;

From 8efdf3141aec9393eb403c9e1780b4b076ae3fc6 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 5 Nov 2023 20:47:56 +0800
Subject: [PATCH 068/101] =?UTF-8?q?code=20review=EF=BC=9Acrm=20=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             | 106 ------------------
 sql/mysql/crm_data.sql                        |   8 +-
 .../admin/product/ProductController.java      |  47 +++-----
 .../admin/product/vo/ProductBaseVO.java       |  13 ++-
 .../admin/product/vo/ProductExcelVO.java      |  11 +-
 .../admin/product/vo/ProductExportReqVO.java  |   8 +-
 .../admin/product/vo/ProductPageReqVO.java    |   9 +-
 .../vo/ProductCategoryBaseVO.java             |  12 +-
 .../vo/ProductCategoryListReqVO.java          |   8 +-
 .../vo/ProductCategoryUpdateReqVO.java        |  12 +-
 .../crm/dal/dataobject/product/ProductDO.java |  15 +--
 .../productcategory/ProductCategoryDO.java    |  12 +-
 .../service/product/ProductServiceImpl.java   |   6 +-
 .../ProductCategoryServiceImpl.java           |  25 +++--
 14 files changed, 87 insertions(+), 205 deletions(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index b11091fbe..e3e2ca0a0 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -1,107 +1 @@
 SET NAMES utf8mb4;
-
-
-
-
--- ----------------------------
--- 回款表
--- ----------------------------
-DROP TABLE IF EXISTS `crm_receivable`;
-CREATE TABLE `crm_receivable`  (
-   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
-   `no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '回款编号',
-   `plan_id` bigint(20) NULL DEFAULT NULL COMMENT '回款计划ID',
-   `customer_id` bigint(20) NULL DEFAULT NULL COMMENT '客户ID',
-   `contract_id` bigint(20) NULL DEFAULT NULL COMMENT '合同ID',
-   `check_status` tinyint(4) NULL DEFAULT NULL COMMENT '审批状态',
-   `process_instance_id` bigint(20) NULL DEFAULT NULL COMMENT '工作流编号',
-   `return_time` datetime NULL DEFAULT NULL COMMENT '回款日期',
-   `return_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '回款方式',
-   `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '回款金额',
-   `owner_user_id` bigint(20) NULL DEFAULT NULL COMMENT '负责人的用户编号',
-   `batch_id` bigint(20) NULL DEFAULT NULL COMMENT '批次',
-   `sort` int(11) NULL DEFAULT NULL COMMENT '显示顺序',
-   `data_scope` tinyint(4) NULL DEFAULT 1 COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
-   `data_scope_dept_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '数据范围(指定部门数组)',
-   `status` tinyint(4) NOT NULL COMMENT '状态(0正常 1停用)',
-   `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
-   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-   `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
-   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回款管理' ROW_FORMAT = DYNAMIC;
-
-
--- ----------------------------
--- 回款计划表
--- ----------------------------
-DROP TABLE IF EXISTS `crm_receivable_plan`;
-CREATE TABLE `crm_receivable_plan`  (
-    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
-    `period` tinyint(4) DEFAULT NULL COMMENT '期数',
-    `receivable_id` bigint(20) NULL DEFAULT NULL COMMENT '回款ID',
-    `status` tinyint(4) NOT NULL COMMENT '完成状态',
-    `check_status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批状态',
-    `process_instance_id` bigint(20) NULL DEFAULT NULL COMMENT '工作流编号',
-    `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '计划回款金额',
-    `return_time` datetime NULL DEFAULT NULL COMMENT '计划回款日期',
-    `remind_days` bigint(20) NULL DEFAULT NULL COMMENT '提前几天提醒',
-    `remind_time` datetime NULL DEFAULT NULL COMMENT '提醒日期',
-    `customer_id` bigint(20) NULL DEFAULT NULL COMMENT '客户ID',
-    `contract_id` bigint(20) NULL DEFAULT NULL COMMENT '合同ID',
-    `owner_user_id` bigint(20) NULL DEFAULT NULL COMMENT '负责人',
-    `sort` int(11) NULL DEFAULT NULL COMMENT '显示顺序',
-    `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
-    `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-    `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
-    PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回款计划' ROW_FORMAT = DYNAMIC;
-
--- ----------------------------
--- 产品表
--- ----------------------------
-DROP TABLE IF EXISTS `crm_product`;
-CREATE TABLE `crm_product`
-(
-    `id`            bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
-    `name`          varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品名称',
-    `no`            varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci  NOT NULL COMMENT '产品编码',
-    `unit`          varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '单位',
-    `price`         bigint(20) NULL DEFAULT 0 COMMENT '价格',
-    `status`        tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态 1-上架 0-下架',
-    `category_id`   bigint(20) NOT NULL COMMENT '产品分类ID',
-    `description`   varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '产品描述',
-    `owner_user_id` bigint(20) NOT NULL COMMENT '负责人的用户编号',
-    `creator`       varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-    `create_time`   datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `updater`       varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-    `update_time`   datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    `tenant_id`     bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
-    `deleted`       bit(1)                                                        NOT NULL DEFAULT b'0' COMMENT '是否删除',
-    PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '产品表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- 产品分类表
--- ----------------------------
-DROP TABLE IF EXISTS `crm_product_category`;
-CREATE TABLE `crm_product_category`
-(
-    `id`          bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
-    `name`        varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
-    `parent_id`   bigint(20) NOT NULL COMMENT '父级id',
-    `creator`     varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-    `create_time` datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `updater`     varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-    `update_time` datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    `tenant_id`   bigint(20) NOT NULL DEFAULT 0 COMMENT '租户编号',
-    `deleted`     bit(1)                                                        NOT NULL DEFAULT b'0' COMMENT '是否删除',
-    PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '产品分类表' ROW_FORMAT = Dynamic;
\ No newline at end of file
diff --git a/sql/mysql/crm_data.sql b/sql/mysql/crm_data.sql
index 149c65268..b5be1e691 100644
--- a/sql/mysql/crm_data.sql
+++ b/sql/mysql/crm_data.sql
@@ -1,5 +1,6 @@
 
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (184, '回款管理审批状态', 'crm_receivable_check_status', 0, '回款管理审批状态(0 未审核 1 审核通过 2 审核拒绝 3 审核中 4 已撤回)', '1', '2023-10-18 21:44:24', '1', '2023-10-18 21:44:24', b'0', '1970-01-01 00:00:00');
+
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (185, '回款管理-回款方式', 'crm_return_type', 0, '回款管理-回款方式', '1', '2023-10-18 21:54:10', '1', '2023-10-18 21:54:10', b'0', '1970-01-01 00:00:00');
 
 
@@ -8,6 +9,7 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1391, 2, '审核拒绝', '2', 'crm_receivable_check_status', 0, 'default', '', ' 2 审核拒绝', '1', '2023-10-18 21:46:58', '1', '2023-10-18 21:47:21', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1392, 3, '审核中', '3', 'crm_receivable_check_status', 0, 'default', '', ' 3 审核中', '1', '2023-10-18 21:47:35', '1', '2023-10-18 21:47:35', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1393, 4, '已撤回', '4', 'crm_receivable_check_status', 0, 'default', '', ' 4 已撤回', '1', '2023-10-18 21:47:46', '1', '2023-10-18 21:47:46', b'0');
+
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1394, 1, '支票', '1', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:29', '1', '2023-10-18 21:54:29', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1395, 2, '现金', '2', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:41', '1', '2023-10-18 21:54:41', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1396, 3, '邮政汇款', '3', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:53', '1', '2023-10-18 21:54:53', b'0');
@@ -16,9 +18,3 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1399, 6, '支付宝', '6', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:38', '1', '2023-10-18 21:55:38', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1400, 7, '微信支付', '7', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:53', '1', '2023-10-18 21:55:53', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1401, 8, '其他', '8', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:56:06', '1', '2023-10-18 21:56:06', b'0');
-
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (604, '产品状态', 'crm_product_status', 0, '', '1', '2023-10-30 21:47:59', '1', '2023-10-30 21:48:45', b'0', '1970-01-01 00:00:00');
-
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1412, 1, '上架', '1', 'crm_product_status', 0, 'success', '', '', '1', '2023-10-30 21:49:34', '1', '2023-10-30 21:49:34', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1411, 0, '下架', '0', 'crm_product_status', 0, 'success', '', '', '1', '2023-10-30 21:49:13', '1', '2023-10-30 21:49:13', b'0');
-
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/ProductController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/ProductController.java
index da55dd0b6..34c832f82 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/ProductController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/ProductController.java
@@ -1,32 +1,28 @@
 package cn.iocoder.yudao.module.crm.controller.admin.product;
 
-import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.security.access.prepost.PreAuthorize;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.Operation;
-
-import javax.validation.constraints.*;
-import javax.validation.*;
-import javax.servlet.http.*;
-import java.util.*;
-import java.io.IOException;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
-
 import cn.iocoder.yudao.module.crm.controller.admin.product.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.product.ProductDO;
 import cn.iocoder.yudao.module.crm.convert.product.ProductConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.product.ProductDO;
 import cn.iocoder.yudao.module.crm.service.product.ProductService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+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
@@ -70,15 +66,6 @@ public class ProductController {
         return success(ProductConvert.INSTANCE.convert(product));
     }
 
-    @GetMapping("/list")
-    @Operation(summary = "获得产品列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('crm:product:query')")
-    public CommonResult<List<ProductRespVO>> getProductList(@RequestParam("ids") Collection<Long> ids) {
-        List<ProductDO> list = productService.getProductList(ids);
-        return success(ProductConvert.INSTANCE.convertList(list));
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得产品分页")
     @PreAuthorize("@ss.hasPermission('crm:product:query')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductBaseVO.java
index e6ba132f7..15718f4c5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductBaseVO.java
@@ -1,12 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import javax.validation.constraints.*;
+import lombok.Data;
 
+import javax.validation.constraints.NotNull;
+
+// TODO @zange:需要加 CRM 前置噢
 /**
  * 产品 Base VO,提供给添加、修改、详细的子 VO 使用
  * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
@@ -14,6 +13,8 @@ import javax.validation.constraints.*;
 @Data
 public class ProductBaseVO {
 
+    // TODO @zange:example 要写哈;主要是接口文档,可以基于 example 可以生产请求参数
+
     @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
     @NotNull(message = "产品名称不能为空")
     private String name;
@@ -39,6 +40,8 @@ public class ProductBaseVO {
     @Schema(description = "产品描述", example = "你说的对")
     private String description;
 
+    // TODO @zange:这个字段只有 create 可以传递,update 不传递;所以放到 create 和 resp 里;
+
     @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31926")
     @NotNull(message = "负责人的用户编号不能为空")
     private Long ownerUserId;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExcelVO.java
index 0e2178f63..b4d46873e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExcelVO.java
@@ -1,16 +1,13 @@
 package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
 
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-
-import com.alibaba.excel.annotation.ExcelProperty;
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
 
+import java.time.LocalDateTime;
 
+// TODO 芋艿:这个导出最后搞
 /**
  * 产品 Excel VO
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExportReqVO.java
index 9c8aa4051..ead2df93f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductExportReqVO.java
@@ -1,14 +1,14 @@
 package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
 
-import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import java.time.LocalDateTime;
+import lombok.Data;
 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;
 
+// TODO 芋艿:这个导出最后搞
 @Schema(description = "管理后台 - 产品 Excel 导出 Request VO,参数和 ProductPageReqVO 是一致的")
 @Data
 public class ProductExportReqVO {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductPageReqVO.java
index 46285b9e3..db2d3f94e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/ProductPageReqVO.java
@@ -1,14 +1,17 @@
 package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
 
-import lombok.*;
-import java.util.*;
-import io.swagger.v3.oas.annotations.media.Schema;
 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;
 
+// TODO @zange:按照需求,裁剪下筛选的字段,目前应该只要 name 和 status
 @Schema(description = "管理后台 - 产品分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryBaseVO.java
index e120a4567..9681520a4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryBaseVO.java
@@ -1,11 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import javax.validation.constraints.*;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
 
 /**
  * 产品分类 Base VO,提供给添加、修改、详细的子 VO 使用
@@ -18,8 +16,8 @@ public class ProductCategoryBaseVO {
     @NotNull(message = "名称不能为空")
     private String name;
 
-    @Schema(description = "父级id", requiredMode = Schema.RequiredMode.REQUIRED, example = "4680")
-    @NotNull(message = "父级id不能为空")
+    @Schema(description = "父级 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "4680")
+    @NotNull(message = "父级 id 不能为空")
     private Long parentId;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryListReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryListReqVO.java
index c29c72148..e74df4a0c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryListReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryListReqVO.java
@@ -6,11 +6,7 @@ import lombok.Data;
 
 import java.time.LocalDateTime;
 
-/**
- * 产品分类 List VO
- *
- * @author ZanGe丶
- */
+// TODO 芋艿:这个导出最后搞;命名应该是按照 ProductExportReqVO 风格
 @Schema(description = "管理后台 - 产品分类列表 Request VO")
 @Data
 public class ProductCategoryListReqVO {
@@ -18,7 +14,7 @@ public class ProductCategoryListReqVO {
     @ExcelProperty("名称")
     private String name;
 
-    @ExcelProperty("父级id")
+    @ExcelProperty("父级 id")
     private Long parentId;
 
     @ExcelProperty("创建时间")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryUpdateReqVO.java
index 48d7bf4f2..82575fbb1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/productcategory/vo/ProductCategoryUpdateReqVO.java
@@ -1,9 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 产品分类更新 Request VO")
 @Data
@@ -11,8 +13,8 @@ import javax.validation.constraints.*;
 @ToString(callSuper = true)
 public class ProductCategoryUpdateReqVO extends ProductCategoryBaseVO {
 
-    @Schema(description = "主键id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23902")
-    @NotNull(message = "主键id不能为空")
+    @Schema(description = "主键 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23902")
+    @NotNull(message = "主键 id 不能为空")
     private Long id;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/ProductDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/ProductDO.java
index c7b8ec009..c44ef7553 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/ProductDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/product/ProductDO.java
@@ -1,11 +1,10 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.product;
 
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import com.baomidou.mybatisplus.annotation.*;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
 
 /**
  * 产品 DO
@@ -23,7 +22,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 public class ProductDO extends BaseDO {
 
     /**
-     * 主键id
+     * 主键 id
      */
     @TableId
     private Long id;
@@ -47,10 +46,12 @@ public class ProductDO extends BaseDO {
      * 状态
      *
      * 枚举 {@link TODO crm_product_status 对应的类}
+     * // TODO @zange:这个写个枚举类,然后 {@link关联下
      */
     private Integer status;
     /**
-     * 产品分类ID
+     * 产品分类 ID
+     * // TODO @zange:这个要写下关联 CategoryDO 的 id 字段;参考下别的模块哈
      */
     private Long categoryId;
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/productcategory/ProductCategoryDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/productcategory/ProductCategoryDO.java
index 9e968ac86..8dcfaac36 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/productcategory/ProductCategoryDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/productcategory/ProductCategoryDO.java
@@ -1,11 +1,10 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.productcategory;
 
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import com.baomidou.mybatisplus.annotation.*;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
 
 /**
  * 产品分类 DO
@@ -32,7 +31,8 @@ public class ProductCategoryDO extends BaseDO {
      */
     private String name;
     /**
-     * 父级id
+     * 父级 id
+     * // TODO @zange:这个要写下关联 CategoryDO 的 id 字段;参考下别的模块哈
      */
     private Long parentId;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductServiceImpl.java
index 82294de85..e9a49d04f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/product/ProductServiceImpl.java
@@ -36,8 +36,9 @@ public class ProductServiceImpl implements ProductService {
 
     @Override
     public Long createProduct(ProductCreateReqVO createReqVO) {
-        //校验产品编号是否存在
+        // 校验产品编号是否存在
         validateProductNo(createReqVO.getNo());
+        // TODO @zange:需要校验 categoryId 是否存在;
         // 插入
         ProductDO product = ProductConvert.INSTANCE.convert(createReqVO);
         productMapper.insert(product);
@@ -49,6 +50,7 @@ public class ProductServiceImpl implements ProductService {
     public void updateProduct(ProductUpdateReqVO updateReqVO) {
         // 校验存在
         validateProductExists(updateReqVO.getId(), updateReqVO.getNo());
+        // TODO @zange:需要校验 categoryId 是否存在;
         // 更新
         ProductDO updateObj = ProductConvert.INSTANCE.convert(updateReqVO);
         productMapper.updateById(updateObj);
@@ -62,6 +64,7 @@ public class ProductServiceImpl implements ProductService {
         productMapper.deleteById(id);
     }
 
+    // TODO @zange:validateProductExists 要不只校验是否存在;然后是否 no 重复,交给 validateProductNo,名字改成 validateProductNoDuplicate,和别的模块保持一致哈;
     private void validateProductExists(Long id, String no) {
         ProductDO product = productMapper.selectById(id);
         if (product == null) {
@@ -101,4 +104,5 @@ public class ProductServiceImpl implements ProductService {
             throw exception(PRODUCT_NO_EXISTS);
         }
     }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryServiceImpl.java
index 7d3d6fa86..01a51d425 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/productcategory/ProductCategoryServiceImpl.java
@@ -1,23 +1,21 @@
 package cn.iocoder.yudao.module.crm.service.productcategory;
 
+import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.ProductCategoryCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.ProductCategoryListReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.ProductCategoryUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.productcategory.ProductCategoryConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.productcategory.ProductCategoryDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.productcategory.ProductCategoryMapper;
 import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
 
-import java.util.*;
-import cn.iocoder.yudao.module.crm.controller.admin.productcategory.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.productcategory.ProductCategoryDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import cn.iocoder.yudao.module.crm.convert.productcategory.ProductCategoryConvert;
-import cn.iocoder.yudao.module.crm.dal.mysql.productcategory.ProductCategoryMapper;
+import javax.annotation.Resource;
+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 cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.collection.ListUtil;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.PRODUCT_CATEGORY_NOT_EXISTS;
 
+// TODO @zange:这个类所在的包,放到 product 下;
 /**
  * 产品分类 Service 实现类
  *
@@ -32,6 +30,7 @@ public class ProductCategoryServiceImpl implements ProductCategoryService {
 
     @Override
     public Long createProductCategory(ProductCategoryCreateReqVO createReqVO) {
+        // TODO zange:参考 mall: ProductCategoryServiceImpl 补充下必要的参数校验;
         // 插入
         ProductCategoryDO productCategory = ProductCategoryConvert.INSTANCE.convert(createReqVO);
         productCategoryMapper.insert(productCategory);
@@ -41,6 +40,7 @@ public class ProductCategoryServiceImpl implements ProductCategoryService {
 
     @Override
     public void updateProductCategory(ProductCategoryUpdateReqVO updateReqVO) {
+        // TODO zange:参考 mall: ProductCategoryServiceImpl 补充下必要的参数校验;
         // 校验存在
         validateProductCategoryExists(updateReqVO.getId());
         // 更新
@@ -50,6 +50,7 @@ public class ProductCategoryServiceImpl implements ProductCategoryService {
 
     @Override
     public void deleteProductCategory(Long id) {
+        // TODO zange:参考 mall: ProductCategoryServiceImpl 补充下必要的参数校验;
         // 校验存在
         validateProductCategoryExists(id);
         // 删除

From 09d45e639348aaf944da43b6628127a0dd05e165 Mon Sep 17 00:00:00 2001
From: jason <2667446@qq.com>
Date: Mon, 6 Nov 2023 11:33:08 +0800
Subject: [PATCH 069/101] =?UTF-8?q?=E8=BD=AC=E8=B4=A6=20-=20=E5=A2=9E?=
 =?UTF-8?q?=E5=8A=A0=E8=BD=AC=E8=B4=A6=E9=80=9A=E7=9F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/pay_wallet.sql                      | 10 +++-
 .../notify/dto/PayTransferNotifyReqDTO.java   | 27 +++++++++++
 .../module/pay/enums/ErrorCodeConstants.java  |  7 ++-
 .../pay/enums/notify/PayNotifyTypeEnum.java   |  1 +
 .../admin/demo/PayDemoOrderController.java    |  4 +-
 .../admin/demo/PayDemoTransferController.java | 15 +++++-
 .../{ => order}/PayDemoOrderCreateReqVO.java  |  5 +-
 .../vo/{ => order}/PayDemoOrderRespVO.java    |  2 +-
 .../pay/convert/demo/PayDemoOrderConvert.java |  4 +-
 .../pay/dal/dataobject/app/PayAppDO.java      |  5 ++
 .../dataobject/notify/PayNotifyTaskDO.java    |  4 ++
 .../pay/service/demo/PayDemoOrderService.java |  2 +-
 .../service/demo/PayDemoOrderServiceImpl.java |  2 +-
 .../service/demo/PayDemoTransferService.java  |  8 ++++
 .../demo/PayDemoTransferServiceImpl.java      | 46 +++++++++++++++++++
 .../service/notify/PayNotifyServiceImpl.java  | 13 ++++++
 .../transfer/PayTransferServiceImpl.java      | 36 +++++++--------
 17 files changed, 160 insertions(+), 31 deletions(-)
 create mode 100644 yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java
 rename yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/{ => order}/PayDemoOrderCreateReqVO.java (76%)
 rename yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/{ => order}/PayDemoOrderRespVO.java (96%)

diff --git a/sql/mysql/pay_wallet.sql b/sql/mysql/pay_wallet.sql
index 291428ffa..9153948bf 100644
--- a/sql/mysql/pay_wallet.sql
+++ b/sql/mysql/pay_wallet.sql
@@ -245,4 +245,12 @@ INSERT INTO system_menu(
 VALUES (
            '转账订单', '', 2, 3, 1117,
            'transfer', 'ep:credit-card', 'pay/transfer/index', 0, 'PayTransfer'
-       );
\ No newline at end of file
+       );
+
+-- 转账通知脚本
+
+ALTER TABLE `pay_app`
+    ADD COLUMN `transfer_notify_url` varchar(1024) NOT NULL COMMENT '转账结果的回调地址' AFTER `refund_notify_url`;
+ALTER TABLE  `pay_notify_task`
+    MODIFY COLUMN `merchant_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '商户订单编号' AFTER `status`,
+    ADD COLUMN `merchant_transfer_id` varchar(64) COMMENT '商户转账单编号' AFTER `merchant_order_id`;
\ No newline at end of file
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java
new file mode 100644
index 000000000..14ba64463
--- /dev/null
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/notify/dto/PayTransferNotifyReqDTO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.pay.api.notify.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 转账单的通知 Request DTO
+ *
+ * @author jason
+ */
+@Data
+public class PayTransferNotifyReqDTO {
+
+    /**
+     * 商户转账单号
+     */
+    @NotEmpty(message = "商户转账单号不能为空")
+    private String merchantTransferId;
+
+    /**
+     * 转账订单编号
+     */
+    @NotNull(message = "转账订单编号不能为空")
+    private Long payTransferId;
+}
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
index 95835a4b6..8fc38e61c 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
@@ -65,12 +65,13 @@ public interface ErrorCodeConstants {
 
     // ========== 转账模块 1-007-009-000 ==========
     ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}");
-    ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账交易单不存在");
+    ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
     ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1_007_009_002, "转账单已成功转账");
     ErrorCode PAY_TRANSFER_EXISTS = new ErrorCode(1_007_009_003, "已经存在转账单");
     ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经存在,请查询转账订单相关状态");
     ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账");
     ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");
+
     // ========== 示例订单 1-007-900-000 ==========
     ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1_007_900_000, "示例订单不存在");
     ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1_007_900_001, "示例订单更新支付状态失败,订单不是【未支付】状态");
@@ -84,4 +85,8 @@ public interface ErrorCodeConstants {
     ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1_007_900_009, "发起退款失败,退款单编号不匹配");
     ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1_007_900_010, "发起退款失败,退款单金额不匹配");
 
+    // ========== 示例转账订单 1-007-901-001 ==========
+    ErrorCode DEMO_TRANSFER_NOT_FOUND = new ErrorCode(1_007_901_001, "示例转账单不存在");
+    ErrorCode DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR = new ErrorCode(1_007_901_002, "转账失败,转账单编号不匹配");
+    ErrorCode DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH = new ErrorCode(1_007_901_003, "转账失败,转账单金额不匹配");
 }
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java
index 8c259d93c..873e015c6 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/notify/PayNotifyTypeEnum.java
@@ -14,6 +14,7 @@ public enum PayNotifyTypeEnum {
 
     ORDER(1, "支付单"),
     REFUND(2, "退款单"),
+    TRANSFER(3, "转账单")
     ;
 
     /**
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java
index 1e3a61eec..60c04c290 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoOrderController.java
@@ -6,8 +6,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoOrderConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
 import cn.iocoder.yudao.module.pay.service.demo.PayDemoOrderService;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java
index f6c7b31c0..ca01a1edb 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoTransferController.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.demo;
 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.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferRespVO;
 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
@@ -14,6 +16,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import javax.annotation.security.PermitAll;
 import javax.validation.Valid;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -33,9 +36,19 @@ public class PayDemoTransferController {
     }
 
     @GetMapping("/page")
-    @Operation(summary = "获得示例订单分页")
+    @Operation(summary = "获得示例转账订单分页")
     public CommonResult<PageResult<PayDemoTransferRespVO>> getDemoTransferPage(@Valid PageParam pageVO) {
         PageResult<PayDemoTransferDO> pageResult = demoTransferService.getDemoTransferPage(pageVO);
         return success(PayDemoTransferConvert.INSTANCE.convertPage(pageResult));
     }
+
+    @PostMapping("/update-status")
+    @Operation(summary = "更新示例转账订单的转账状态") // 由 pay-module 转账服务,进行回调
+    @PermitAll // 无需登录,安全由 PayDemoTransferService 内部校验实现
+    @OperateLog(enable = false) // 禁用操作日志,因为没有操作人
+    public CommonResult<Boolean> updateDemoTransferStatus(@RequestBody PayTransferNotifyReqDTO notifyReqDTO) {
+        demoTransferService.updateDemoTransferStatus(Long.valueOf(notifyReqDTO.getMerchantTransferId()),
+                notifyReqDTO.getPayTransferId());
+        return success(true);
+    }
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java
similarity index 76%
rename from yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java
rename to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java
index 9960ada48..6c82a0ab6 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderCreateReqVO.java
@@ -1,9 +1,8 @@
-package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
+package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order;
 
-import lombok.*;
 import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
 
-import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - 示例订单创建 Request VO")
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java
similarity index 96%
rename from yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java
rename to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java
index 3404844dc..cb305631d 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/vo/order/PayDemoOrderRespVO.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.pay.controller.admin.demo.vo;
+package cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java
index 313e5d266..8fca99791 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/demo/PayDemoOrderConvert.java
@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.pay.convert.demo;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderRespVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
index 977eff93a..8f3490fc7 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
@@ -54,4 +54,9 @@ public class PayAppDO extends BaseDO {
      */
     private String refundNotifyUrl;
 
+    /**
+     * 转账结果的回调地址
+     */
+    private String transferNotifyUrl;
+
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java
index 181a32802..7bfabad3f 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java
@@ -66,6 +66,10 @@ public class PayNotifyTaskDO extends TenantBaseDO {
      * 商户订单编号
      */
     private String merchantOrderId;
+    /**
+     * 商户转账单编号
+     */
+    private String merchantTransferId;
     /**
      * 通知状态
      *
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java
index e6822e626..cf870253f 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderService.java
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.pay.service.demo;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
 
 import javax.validation.Valid;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
index 7e1090248..57ff80637 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
@@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
 import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
-import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.order.PayDemoOrderCreateReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java
index 9116dcd9a..fb01ec43d 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferService.java
@@ -28,4 +28,12 @@ public interface PayDemoTransferService {
      * @param pageVO 分页查询参数
      */
     PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO);
+
+    /**
+     * 更新转账业务示例订单的转账状态
+     *
+     * @param id 编号
+     * @param payTransferId 转账单编号
+     */
+    void updateDemoTransferStatus(Long id, Long payTransferId);
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
index e892e4446..8de98da1e 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoTransferServiceImpl.java
@@ -1,18 +1,25 @@
 package cn.iocoder.yudao.module.pay.service.demo;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.pay.controller.admin.demo.vo.transfer.PayDemoTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.convert.demo.PayDemoTransferConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoTransferDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoTransferMapper;
+import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
+import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
 import javax.validation.Validator;
+import java.util.Objects;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum.WAITING;
 
 /**
@@ -33,6 +40,8 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
     @Resource
     private PayDemoTransferMapper demoTransferMapper;
     @Resource
+    private PayTransferService payTransferService;
+    @Resource
     private Validator validator;
 
     @Override
@@ -50,4 +59,41 @@ public class PayDemoTransferServiceImpl implements PayDemoTransferService {
     public PageResult<PayDemoTransferDO> getDemoTransferPage(PageParam pageVO) {
         return demoTransferMapper.selectPage(pageVO);
     }
+
+    @Override
+    public void updateDemoTransferStatus(Long id, Long payTransferId) {
+        PayTransferDO payTransfer = validateDemoTransferStatusCanUpdate(id, payTransferId);
+        // 更新示例订单状态
+        if (payTransfer != null) {
+            demoTransferMapper.updateById(new PayDemoTransferDO().setId(id)
+                    .setPayTransferId(payTransferId)
+                    .setPayChannelCode(payTransfer.getChannelCode())
+                    .setTransferStatus(payTransfer.getStatus())
+                    .setTransferTime(payTransfer.getSuccessTime()));
+        }
+    }
+
+    private PayTransferDO validateDemoTransferStatusCanUpdate(Long id, Long payTransferId) {
+        PayDemoTransferDO demoTransfer = demoTransferMapper.selectById(id);
+        if (demoTransfer == null) {
+            throw exception(DEMO_TRANSFER_NOT_FOUND);
+        }
+        if (PayTransferStatusEnum.isSuccess(demoTransfer.getTransferStatus())
+                || PayTransferStatusEnum.isClosed(demoTransfer.getTransferStatus())) {
+            // 无需更新返回 null
+            return null;
+        }
+        PayTransferDO transfer = payTransferService.getTransfer(payTransferId);
+        if (transfer == null) {
+            throw exception(PAY_TRANSFER_NOT_FOUND);
+        }
+        if (!Objects.equals(demoTransfer.getPrice(), transfer.getPrice())) {
+            throw exception(DEMO_TRANSFER_FAIL_PRICE_NOT_MATCH);
+        }
+        if (ObjectUtil.notEqual(transfer.getMerchantTransferId(), id.toString())) {
+            throw exception(DEMO_TRANSFER_FAIL_TRANSFER_ID_ERROR);
+        }
+        // TODO 校验账号
+        return transfer;
+    }
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
index f6b17c565..1d356cf49 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/notify/PayNotifyServiceImpl.java
@@ -13,11 +13,13 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
 import cn.iocoder.yudao.module.pay.api.notify.dto.PayRefundNotifyReqDTO;
+import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyLogDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.notify.PayNotifyTaskDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyLogMapper;
 import cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper;
 import cn.iocoder.yudao.module.pay.dal.redis.notify.PayNotifyLockRedisDAO;
@@ -25,6 +27,7 @@ import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
+import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
 import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
@@ -73,6 +76,9 @@ public class PayNotifyServiceImpl implements PayNotifyService {
     @Resource
     @Lazy // 循环依赖,避免报错
     private PayRefundService refundService;
+    @Resource
+    @Lazy // 循环依赖,避免报错
+    private PayTransferService transferService;
 
     @Resource
     private PayNotifyTaskMapper notifyTaskMapper;
@@ -100,6 +106,10 @@ public class PayNotifyServiceImpl implements PayNotifyService {
             PayRefundDO refundDO = refundService.getRefund(task.getDataId());
             task.setAppId(refundDO.getAppId())
                     .setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
+        } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
+            PayTransferDO transfer = transferService.getTransfer(task.getDataId());
+            task.setAppId(transfer.getAppId()).setMerchantTransferId(transfer.getMerchantTransferId())
+                    .setNotifyUrl(transfer.getNotifyUrl());
         }
 
         // 执行插入
@@ -214,6 +224,9 @@ public class PayNotifyServiceImpl implements PayNotifyService {
         } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
             request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId())
                     .payRefundId(task.getDataId()).build();
+        } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.TRANSFER.getType())) {
+            request = new PayTransferNotifyReqDTO().setMerchantTransferId(task.getMerchantTransferId())
+                    .setPayTransferId(task.getDataId());
         } else {
             throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task));
         }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
index 014a7aae7..27b9807bd 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
@@ -12,12 +12,15 @@ import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespE
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
+import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper;
 import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
+import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
+import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -51,6 +54,8 @@ public class PayTransferServiceImpl implements PayTransferService {
     @Resource
     private PayChannelService channelService;
     @Resource
+    private PayNotifyService notifyService;
+    @Resource
     private PayNoRedisDAO noRedisDAO;
     @Resource
     private Validator validator;
@@ -73,7 +78,7 @@ public class PayTransferServiceImpl implements PayTransferService {
         // 1.1 校验转账单是否可以提交
         validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId());
         // 1.2 校验 App
-        appService.validPayApp(reqDTO.getAppId());
+        PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
         // 1.3 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
         PayClient client = channelService.getPayClient(channel.getId());
@@ -86,7 +91,7 @@ public class PayTransferServiceImpl implements PayTransferService {
         PayTransferDO transfer = INSTANCE.convert(reqDTO)
                 .setChannelId(channel.getId())
                 .setNo(no).setStatus(WAITING.getStatus())
-                .setNotifyUrl("http://127.0.0.1:48080/admin-api/pay/todo"); // TODO 需要加个transfer Notify url
+                .setNotifyUrl(payApp.getTransferNotifyUrl());
         transferMapper.insert(transfer);
         PayTransferRespDTO unifiedTransferResp = null;
         try {
@@ -145,22 +150,13 @@ public class PayTransferServiceImpl implements PayTransferService {
     }
 
     private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
-        // 1. 更新 PayTransferDO 转账成功
-        Boolean transferred = updateTransferSuccess(channel, notify);
-        if (transferred) {
-            return;
-        }
-        // 2. TODO 插入转账通知记录
-    }
-
-    private Boolean updateTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
         PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
         if (transfer == null) {
             throw exception(PAY_TRANSFER_NOT_FOUND);
         }
         if (isSuccess(transfer.getStatus())) { // 如果已成功,直接返回,不用重复更新
-            return Boolean.TRUE;
+            return;
         }
         if (!isPendingStatus(transfer.getStatus())) {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
@@ -176,10 +172,13 @@ public class PayTransferServiceImpl implements PayTransferService {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
         }
         log.info("[updateTransferSuccess][transfer({}) 更新为已转账]", transfer.getId());
-        return Boolean.FALSE;
+
+        // 3. 插入转账通知记录
+        notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
+                transfer.getId());
     }
 
-    private void updateTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
+    private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
         PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
         if (transfer == null) {
@@ -192,6 +191,7 @@ public class PayTransferServiceImpl implements PayTransferService {
         if (!isPendingStatus(transfer.getStatus())) {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
         }
+
         // 2.更新
         int updateCount = transferMapper.updateByIdAndStatus(transfer.getId(),
                 CollUtil.newArrayList(WAITING.getStatus(), IN_PROGRESS.getStatus()),
@@ -203,11 +203,11 @@ public class PayTransferServiceImpl implements PayTransferService {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
         }
         log.info("[updateTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
-    }
 
-    private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
-        //  更新 PayTransferDO 转账关闭
-        updateTransferClosed(channel, notify);
+        // 3. 插入转账通知记录
+        notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
+                transfer.getId());
+
     }
 
     /**

From a24919e55506bf5cdf98d40563a31fecfe04892d Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Mon, 6 Nov 2023 21:59:23 +0800
Subject: [PATCH 070/101] =?UTF-8?q?style:=20=E5=AE=A2=E6=88=B7=20review=20?=
 =?UTF-8?q?=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/customer/CrmCustomerController.java | 21 ++++-------------
 .../convert/customer/CrmCustomerConvert.java  | 23 +++++++++++++++++++
 2 files changed, 27 insertions(+), 17 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 10b6b5ee8..3c7dbeb7a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -5,6 +5,8 @@ import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
@@ -101,25 +103,10 @@ public class CrmCustomerController {
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
         PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO);
-        PageResult<CrmCustomerRespVO> pageVo = CrmCustomerConvert.INSTANCE.convertPage(pageResult);
-        // TODO @wanwan: 可以参考 CollectionUtils.convertListByFlatMap(),目的是简洁
-        Set<Long> userSet = pageVo.getList().stream().flatMap(i -> Stream.of(NumberUtil.parseLong(i.getCreator()), i.getOwnerUserId())).collect(Collectors.toSet());
+        Set<Long> userSet = CollectionUtils.convertSetByFlatMap(pageResult.getList(), i -> Stream.of(NumberUtil.parseLong(i.getCreator()), i.getOwnerUserId()));
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userSet);
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(userMap.values().stream().map(AdminUserRespDTO::getDeptId).collect(Collectors.toSet()));
-        // TODO @wanwan:这块可以形成一个 convertPage 方法,default 实现;
-        pageVo.getList().forEach(customerRespVO -> {
-            customerRespVO.setAreaName(AreaUtils.format(customerRespVO.getAreaId()));
-            customerRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(customerRespVO.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
-            // TODO @wanwan:可以使用 MapUtils.findAndThen
-            AdminUserRespDTO ownerUser = userMap.get(customerRespVO.getOwnerUserId());
-            if (Objects.nonNull(ownerUser)) {
-                customerRespVO.setOwnerUserName(ownerUser.getNickname());
-                DeptRespDTO dept = deptMap.get(ownerUser.getDeptId());
-                if (Objects.nonNull(dept)) {
-                    customerRespVO.setOwnerUserDept(dept.getName());
-                }
-            }
-        });
+        PageResult<CrmCustomerRespVO> pageVo = CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap);
         return success(pageVo);
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index 6aa86218c..ffb466208 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -1,15 +1,23 @@
 package cn.iocoder.yudao.module.crm.convert.customer;
 
+import cn.hutool.core.util.NumberUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
+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.Mapping;
 import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 
 /**
  * 客户 Convert
@@ -29,6 +37,21 @@ public interface CrmCustomerConvert {
 
     PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> page);
 
+    default PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> page, Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
+        PageResult<CrmCustomerRespVO> result = convertPage(page);
+        result.getList().forEach(customerRespVO -> {
+            customerRespVO.setAreaName(AreaUtils.format(customerRespVO.getAreaId()));
+            MapUtils.findAndThen(userMap, NumberUtil.parseLong(customerRespVO.getCreator()), creator ->
+                    customerRespVO.setCreatorName(creator.getNickname()));
+            MapUtils.findAndThen(userMap, customerRespVO.getOwnerUserId(), ownerUser -> {
+                customerRespVO.setOwnerUserName(ownerUser.getNickname());
+                MapUtils.findAndThen(deptMap, ownerUser.getDeptId(), dept ->
+                        customerRespVO.setOwnerUserDept(dept.getName()));
+            });
+        });
+        return result;
+    }
+
     List<CrmCustomerExcelVO> convertList02(List<CrmCustomerDO> list);
 
     @Mappings({

From 668fae9398e95e67530f81606363750729029c87 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Tue, 7 Nov 2023 17:52:18 +0800
Subject: [PATCH 071/101] =?UTF-8?q?CRM-=E6=95=B0=E6=8D=AE=E6=9D=83?=
 =?UTF-8?q?=E9=99=90=EF=BC=9A=E5=AE=8C=E5=96=84=20code=20review=20?=
 =?UTF-8?q?=E6=8F=90=E5=88=B0=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../business/vo/CrmBusinessTransferReqVO.java |  12 +-
 .../contact/vo/CrmContactTransferReqVO.java   |  11 +-
 .../contract/vo/CrmContractTransferReqVO.java |  12 +-
 .../customer/vo/CrmCustomerTransferReqVO.java |  12 +-
 .../permission/CrmPermissionController.http   |  32 ++++
 .../permission/CrmPermissionController.java   |  93 +++------
 .../permission/vo/CrmPermissionBaseVO.java    |   3 +-
 .../permission/vo/CrmPermissionRespVO.java    |  13 +-
 .../vo/CrmPermissionUpdateReqVO.java          |   6 +-
 .../convert/business/CrmBusinessConvert.java  |   4 +-
 .../crm/convert/contact/ContactConvert.java   |   4 +-
 .../crm/convert/contract/ContractConvert.java |   4 +-
 .../convert/customer/CrmCustomerConvert.java  |   4 +-
 .../permission/CrmPermissionConvert.java      |  16 +-
 .../permission/CrmPermissionDO.java           |   5 +-
 .../mysql/permission/CrmPermissionMapper.java |  18 --
 .../core/annotations/CrmPermission.java       |  18 +-
 .../core/aop/CrmPermissionAspect.java         | 179 +++++++++++-------
 .../crm/framework/core/config/SpelConfig.java |  20 ++
 .../service/CrmPermissionValidateService.java |  20 --
 .../crm/framework/enums/CrmBizTypeEnum.java   |  12 +-
 .../enums/CrmPermissionLevelEnum.java         |  10 -
 .../service/business/CrmBusinessService.java  |   3 +-
 .../business/CrmBusinessServiceImpl.java      |  21 +-
 .../service/contact/ContactServiceImpl.java   |   9 +-
 .../service/contract/ContractServiceImpl.java |   9 +-
 .../customer/CrmCustomerServiceImpl.java      |  13 +-
 .../permission/CrmPermissionService.java      |   6 +-
 .../permission/CrmPermissionServiceImpl.java  |  35 ++--
 .../bo/CrmPermissionCreateReqBO.java          |   3 +-
 ...O.java => CrmPermissionTransferReqBO.java} |  14 +-
 .../bo/CrmPermissionUpdateReqBO.java          |  18 +-
 .../yudao/module/system/api/dept/PostApi.java |  12 ++
 .../system/api/dept/dto/PostRespDTO.java      |  37 ++++
 .../module/system/api/dept/PostApiImpl.java   |   8 +
 .../system/convert/dept/PostConvert.java      |   3 +
 36 files changed, 346 insertions(+), 353 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.http
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/config/SpelConfig.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/service/CrmPermissionValidateService.java
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/{CrmTransferPermissionReqBO.java => CrmPermissionTransferReqBO.java} (64%)
 create mode 100644 yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/dto/PostRespDTO.java

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
index fc1ab74c6..db24abcf8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
@@ -21,19 +21,11 @@ public class CrmBusinessTransferReqVO {
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
-    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
-     * 老负责人是否加入团队,是/否
-     */
-    @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
-    @NotNull(message = "老负责人是否加入团队不能为空")
-    private Boolean joinTeam;
-
-    /**
-     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
      * 关联 {@link CrmPermissionLevelEnum}
      */
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    private Integer permissionLevel;
+    private Integer oldOwnerPermissionLevel;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
index 6ca37d138..32bdcd754 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
@@ -21,18 +21,11 @@ public class CrmContactTransferReqVO {
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
-    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
-     * 老负责人是否加入团队,是/否
-     */
-    @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
-    @NotNull(message = "老负责人是否加入团队不能为空")
-    private Boolean joinTeam;
-    /**
-     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
      * 关联 {@link CrmPermissionLevelEnum}
      */
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    private Integer permissionLevel;
+    private Integer oldOwnerPermissionLevel;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
index 241693cd3..95691b5a8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
@@ -21,19 +21,11 @@ public class CrmContractTransferReqVO {
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
-    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
-     * 老负责人是否加入团队,是/否
-     */
-    @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
-    @NotNull(message = "老负责人是否加入团队不能为空")
-    private Boolean joinTeam;
-
-    /**
-     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
      * 关联 {@link CrmPermissionLevelEnum}
      */
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    private Integer permissionLevel;
+    private Integer oldOwnerPermissionLevel;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
index 990eaed3d..23df716c2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
@@ -21,19 +21,11 @@ public class CrmCustomerTransferReqVO {
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
     private Long newOwnerUserId;
 
-    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
-     * 老负责人是否加入团队,是/否
-     */
-    @Schema(description = "老负责人是否加入团队", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
-    @NotNull(message = "老负责人是否加入团队不能为空")
-    private Boolean joinTeam;
-
-    /**
-     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
      * 关联 {@link CrmPermissionLevelEnum}
      */
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    private Integer permissionLevel;
+    private Integer oldOwnerPermissionLevel;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.http
new file mode 100644
index 000000000..1a7faecdd
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.http
@@ -0,0 +1,32 @@
+### 请求 /add
+PUT {{baseUrl}}/crm/permission/add
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "userId": 1,
+  "bizType": 2,
+  "bizId": 2,
+  "level": 1
+}
+
+### 请求 /update
+PUT {{baseUrl}}/crm/permission/update
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "userId": 1,
+  "bizType": 2,
+  "bizId": 2,
+  "level": 1,
+  "id": 1
+}
+
+### 请求 /delete
+DELETE {{baseUrl}}/crm/permission/delete?bizType=2&bizId=1&id=1
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
index 8402b4f49..594e132bf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
@@ -8,8 +8,14 @@ import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionR
 import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.framework.core.service.CrmPermissionValidateService;
+import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.PostApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
@@ -24,15 +30,12 @@ import javax.annotation.Resource;
 import javax.validation.Valid;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
-import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_DENIED;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_MODEL_NOT_EXISTS;
-import static cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum.getNameByType;
-import static cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum.isOwner;
 
 @Tag(name = "管理后台 - CRM 数据权限(数据团队成员操作)")
 @RestController
@@ -46,34 +49,16 @@ public class CrmPermissionController {
     @Resource
     private AdminUserApi adminUserApi;
     @Resource
-    private List<CrmPermissionValidateService> permissionValidateServices;
-
-    // TODO @puhui999:这个能不能使用 CrmPermission 注解替代?
-    private void validatePermission(Integer bizType, Long bizId) {
-        // 1. TODO 校验是否为超级管理员
-        // 2. 防御一手,如果是超级管理员不校验权限还是得校验一下数据是否存在
-        // TODO @puhui999:是不是不用校验每个业务方的数据是否存在;其实不是很关键哈;简单一点~ 说白了,负责人只要在,它的数据就是存在~
-        permissionValidateServices.forEach(item -> {
-            if (!item.validateBizIdExists(bizType, bizId)) {
-                throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, getNameByType(bizType));
-            }
-        });
-        // 3. 校验数据权限 (如果存在则表示 bizId 也存在)
-        CrmPermissionDO permission = crmPermissionService.getPermissionByBizTypeAndBizIdAndUserId(
-                bizType, bizId, getLoginUserId());
-        if (isOwner(permission.getPermissionLevel())) { // 只有负责人才可以操作团队成员
-            return;
-        }
-        throw exception(CRM_PERMISSION_DENIED, getNameByType(bizType));
-    }
+    private DeptApi deptApi;
+    @Resource
+    private PostApi postApi;
 
     @PutMapping("/add")
     @Operation(summary = "添加团队成员")
     @PreAuthorize("@ss.hasPermission('crm:permission:create')")
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#reqVO.bizType", bizId = "#reqVO.bizId"
+            , level = CrmPermissionLevelEnum.OWNER)
     public CommonResult<Boolean> addPermission(@Valid @RequestBody CrmPermissionCreateReqVO reqVO) {
-        // 1. 前置校验
-        validatePermission(reqVO.getBizType(), reqVO.getBizId());
-
         // 2. 加入成员
         crmPermissionService.createPermission(CrmPermissionConvert.INSTANCE.convert(reqVO));
         return success(true);
@@ -83,53 +68,30 @@ public class CrmPermissionController {
     @PutMapping("/update")
     @Operation(summary = "编辑团队成员")
     @PreAuthorize("@ss.hasPermission('crm:permission:update')")
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#updateReqVO.bizType", bizId = "#updateReqVO.bizId"
+            , level = CrmPermissionLevelEnum.WRITE)
     public CommonResult<Boolean> updatePermission(@Valid @RequestBody CrmPermissionUpdateReqVO updateReqVO) {
-        // 1. 前置校验
-        validatePermission(updateReqVO.getBizType(), updateReqVO.getBizId());
-
-        // 2. 编辑团队成员
         crmPermissionService.updatePermission(CrmPermissionConvert.INSTANCE.convert(updateReqVO));
         return success(true);
     }
 
-    // TODO @puhui999:deletemapping
-    @GetMapping("/delete")
+    @DeleteMapping("/delete")
     @Operation(summary = "移除团队成员")
-    @Parameter(name = "id", description = "团队成员编号", required = true)
-    // TODO @puhui999:是不是 id 参数就够了?
+    @Parameters({
+            @Parameter(name = "bizType", description = "CRM 类型", required = true, example = "2"),
+            @Parameter(name = "bizId", description = "CRM 类型数据编号", required = true, example = "1024"),
+            @Parameter(name = "id", description = "团队成员编号", required = true, example = "1024")
+    })
     @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#bizType", bizId = "#bizId"
+            , level = CrmPermissionLevelEnum.OWNER)
     public CommonResult<Boolean> deletePermission(@RequestParam("bizType") Integer bizType,
                                                   @RequestParam("bizId") Long bizId,
                                                   @RequestParam("id") Long id) {
-        // 1. 前置校验
-        validatePermission(bizType, bizId);
-
-        // 2. 移除团队成员
         crmPermissionService.deletePermission(id);
         return success(true);
     }
 
-    // TODO @puhui999:这个是哪个地方使用到哈?
-    // TODO @puhui999:是不是 deletemapping 呀;
-    @GetMapping("/quit")
-    @Operation(summary = "退出团队")
-    @Parameters({
-            @Parameter(name = "bizType", description = "CRM 类型", required = true, example = "2"),
-            @Parameter(name = "bizId", description = "CRM 类型数据编号", required = true, example = "1024")
-    })
-    @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
-    public CommonResult<Boolean> quitPermission(@RequestParam("bizType") Integer bizType,
-                                                @RequestParam("bizId") Long bizId) {
-        // 没有就不是团队成员
-        CrmPermissionDO permission = crmPermissionService.getPermissionByBizTypeAndBizIdAndUserId(
-                bizType, bizId, getLoginUserId());
-        if (permission == null) {
-            return success(false);
-        }
-        crmPermissionService.deletePermission(permission.getId());
-        return success(true);
-    }
-
     @GetMapping("/list")
     @Operation(summary = "获取团队成员")
     @Parameters({
@@ -148,7 +110,10 @@ public class CrmPermissionController {
 
         // 拼接数据
         List<AdminUserRespDTO> userList = adminUserApi.getUserList(convertSet(permission, CrmPermissionDO::getUserId));
-        return success(CrmPermissionConvert.INSTANCE.convert(permission, userList));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userList, AdminUserRespDTO::getDeptId));
+        Set<Long> postIds = userList.stream().flatMap(item -> item.getPostIds().stream()).collect(Collectors.toSet());
+        Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
+        return success(CrmPermissionConvert.INSTANCE.convert(permission, userList, deptMap, postMap));
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
index 98ac5831d..4ad70859f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
@@ -30,10 +30,9 @@ public class CrmPermissionBaseVO {
     @NotNull(message = "Crm 类型数据编号不能为空")
     private Long bizId;
 
-    // TODO @puhui999:level;
     @Schema(description = "权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     @InEnum(CrmPermissionLevelEnum.class)
     @NotNull(message = "权限级别不能为空")
-    private Integer permissionLevel;
+    private Integer level;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
index 9b7df8c16..a05dfd61a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
@@ -12,18 +12,13 @@ public class CrmPermissionRespVO extends CrmPermissionBaseVO {
     @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     private Long id;
 
-    // TODO @puhui999:搞到字典里;
-    @Schema(description = "团队级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "负责人")
-    private String permissionLevelName;
-
-    // TODO @puhui999:deptId、postIds 是不是要提供中文名哈;
-    @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    private Long deptId;
+    @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部")
+    private String deptName;
 
     @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
     private String nickname;
 
-    @Schema(description = "岗位编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]")
-    private Set<Long> postIds;
+    @Schema(description = "岗位名称数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[BOOS,经理]")
+    private Set<String> postNames;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
index 328a774f1..2c83e0751 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
@@ -5,6 +5,8 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
+import javax.validation.constraints.NotNull;
+
 @Schema(description = "管理后台 - CRM 数据权限更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -12,9 +14,9 @@ import lombok.ToString;
 public class CrmPermissionUpdateReqVO extends CrmPermissionBaseVO {
 
     @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
-    // TODO @puhui999:非空判断;
+    @NotNull(message = "数据权限编号不能为空")
     private Long id;
 
-    // TODO @puhui999:是不是只更新 permission???
+    // TODO @puhui999:是不是只更新 permission??? 是的
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index 9e23c793a..4256a6463 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
@@ -36,7 +36,7 @@ public interface CrmBusinessConvert {
             @Mapping(target = "bizId", source = "reqVO.id"),
             @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    CrmTransferPermissionReqBO convert(CrmBusinessTransferReqVO reqVO, Long userId);
+    CrmPermissionTransferReqBO convert(CrmBusinessTransferReqVO reqVO, Long userId);
 
     CrmPermissionPageReqBO convert(CrmBusinessPageReqVO pageReqVO);
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index b4cc69963..3e703c22e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.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.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
@@ -37,6 +37,6 @@ public interface ContactConvert {
             @Mapping(target = "bizId", source = "reqVO.id"),
             @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    CrmTransferPermissionReqBO convert(CrmContactTransferReqVO reqVO, Long userId);
+    CrmPermissionTransferReqBO convert(CrmContactTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
index 7f629ef8c..09c61dd6d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/ContractConvert.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.contract;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
@@ -37,6 +37,6 @@ public interface ContractConvert {
             @Mapping(target = "bizId", source = "reqVO.id"),
             @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    CrmTransferPermissionReqBO convert(CrmContractTransferReqVO reqVO, Long userId);
+    CrmPermissionTransferReqBO convert(CrmContractTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index 6aa86218c..f8b47ef19 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.crm.convert.customer;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
@@ -35,6 +35,6 @@ public interface CrmCustomerConvert {
             @Mapping(target = "bizId", source = "reqVO.id"),
             @Mapping(target = "newOwnerUserId", source = "reqVO.id")
     })
-    CrmTransferPermissionReqBO convert(CrmCustomerTransferReqVO reqVO, Long userId);
+    CrmPermissionTransferReqBO convert(CrmCustomerTransferReqVO reqVO, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
index 9fa7e5fad..147cdd01a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
@@ -8,15 +8,16 @@ import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionU
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import com.google.common.collect.Multimaps;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
 import java.util.Map;
 
-import static cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum.getNameByLevel;
-
 /**
  * Crm 数据权限 Convert
  *
@@ -37,12 +38,17 @@ public interface CrmPermissionConvert {
 
     List<CrmPermissionRespVO> convert(List<CrmPermissionDO> permission);
 
-    default List<CrmPermissionRespVO> convert(List<CrmPermissionDO> permission, List<AdminUserRespDTO> userList) {
+    default List<CrmPermissionRespVO> convert(List<CrmPermissionDO> permission, List<AdminUserRespDTO> userList,
+                                              Map<Long, DeptRespDTO> deptMap, Map<Long, PostRespDTO> postMap) {
         Map<Long, AdminUserRespDTO> userMap = CollectionUtils.convertMap(userList, AdminUserRespDTO::getId);
         return CollectionUtils.convertList(convert(permission), item -> {
             MapUtils.findAndThen(userMap, item.getId(), user -> {
-                item.setNickname(user.getNickname()).setDeptId(user.getDeptId()).setPostIds(user.getPostIds())
-                        .setPermissionLevelName(getNameByLevel(item.getPermissionLevel()));
+                item.setNickname(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), deptRespDTO -> {
+                    item.setDeptName(deptRespDTO.getName());
+                });
+                List<PostRespDTO> postRespList = MapUtils.getList(Multimaps.forMap(postMap), user.getPostIds());
+                item.setPostNames(CollectionUtils.convertSet(postRespList, PostRespDTO::getName));
             });
             return item;
         });
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
index 263a8fd7a..aa3b1e64f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
@@ -23,7 +23,7 @@ import lombok.*;
 @AllArgsConstructor
 public class CrmPermissionDO extends BaseDO {
 
-    // TODO puhui999:是不是公海的数据,就不插入了;
+    // TODO puhui999:是不是公海的数据,就不插入了;这样方便获取公海数据鸭
     /**
      * 当数据变为公海数据时,也就是数据团队成员中没有负责人的时候,将原本的负责人 userId 设置为 POOL_USER_ID 方便查询公海数据。
      * 也就是说每条数据到最后都有一个负责人,如果有人领取则 userId 为领取人
@@ -59,12 +59,11 @@ public class CrmPermissionDO extends BaseDO {
      */
     private Long userId;
 
-    // TODO @puhui999:是不是搞成 level 字段;简洁一点,主要表明已经 perssmion 实体里了;
     /**
      * 权限级别
      *
      * 关联 {@link CrmPermissionLevelEnum}
      */
-    private Integer permissionLevel;
+    private Integer level;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index 3d006d0f8..3b9a220fc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -4,7 +4,6 @@ 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.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -18,15 +17,6 @@ import java.util.List;
 @Mapper
 public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
 
-    // TODO @puhui999:是不是不用谢这个注释;因为方法名,可以自解释;
-    /**
-     * 获取用户数据权限通过 数据类型 x 某个数据 x 用户编号
-     *
-     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
-     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
-     * @param userId  用户编号,AdminUser#id
-     * @return Crm 数据权限
-     */
     default CrmPermissionDO selectByBizTypeAndBizIdByUserId(Integer bizType, Long bizId, Long userId) {
         return selectOne(new LambdaQueryWrapperX<CrmPermissionDO>()
                 .eq(CrmPermissionDO::getBizType, bizType)
@@ -34,14 +24,6 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 .eq(CrmPermissionDO::getUserId, userId));
     }
 
-    // TODO @puhui999:是不是不用谢这个注释;因为方法名,可以自解释;
-    /**
-     * 获取数据权限列表通过 数据类型 x 某个数据
-     *
-     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
-     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
-     * @return Crm 数据权限列表
-     */
     default List<CrmPermissionDO> selectByBizTypeAndBizId(Integer bizType, Long bizId) {
         return selectList(new LambdaQueryWrapperX<CrmPermissionDO>()
                 .eq(CrmPermissionDO::getBizType, bizType)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
index d63ecf3ee..5ef382b74 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
@@ -26,17 +26,21 @@ public @interface CrmPermission {
      */
     CrmBizTypeEnum bizType();
 
-    // TODO @puhui999:id,通过 spring el 表达式获取;
     /**
-     * 数据编号获取来源类,确保数据 id 编号在此类中,不能在父类中。
-     * 例:如果在 baseVO 中需要把 id 弄到 updateVO 中。
+     * crm 类型扩展
+     * 用于 CrmPermissionController 团队权限校验
      */
-    Class<?>[] getIdFor() default {};
+    String bizTypeValue() default "";
 
-    // TODO @puhui999:是不是搞成 level 字段;简洁一点,主要表明已经 perssmion 实体里了;
     /**
-     * 操作类型
+     * 数据编号,通过 spring el 表达式获取
+     * TODO 数据权限完成后去除 default ""
      */
-    CrmPermissionLevelEnum permissionLevel();
+    String bizId() default "";
+
+    /**
+     * 操作所需权限级别
+     */
+    CrmPermissionLevelEnum level();
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index 497c7caba..c306bd78a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -2,24 +2,32 @@ package cn.iocoder.yudao.module.crm.framework.core.aop;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
-import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_DENIED;
-import static cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum.*;
 
 /**
  * Crm 数据权限校验 AOP 切面
@@ -31,6 +39,11 @@ import static cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum
 @Slf4j
 public class CrmPermissionAspect {
 
+    @Resource
+    private LocalVariableTableParameterNameDiscoverer discoverer;
+    @Resource
+    private SpelExpressionParser parser;
+
     @Resource
     private CrmPermissionService crmPermissionService;
 
@@ -43,83 +56,109 @@ public class CrmPermissionAspect {
         return WebFrameworkUtils.getLoginUserId();
     }
 
-    // TODO @puhui999:id,通过 spring el 表达式获取;
-    private Long getBizId(JoinPoint joinPoint, CrmPermission crmPermission) throws NoSuchFieldException, IllegalAccessException {
-        Object[] args = joinPoint.getArgs();
-        for (Object arg : args) {
-            if (arg == null) {
-                continue;
-            }
-            if (ObjUtil.notEqual(arg.getClass().getName(), crmPermission.getIdFor()[0].getName())) {
-                continue;
-            }
-            // 使用反射获取id属性
-            Field idValue = arg.getClass().getDeclaredField("id");
-            // 设置字段为可访问
-            idValue.setAccessible(true);
-            return (Long) idValue.get(arg);
-        }
-        return null;
-    }
-
-    // TODO @puhui999:一般核心的方法,放到最前面,private 放后面。主要是,主次要分出来哈;
     @Before("@annotation(crmPermission)")
-    public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
+    public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) throws NoSuchMethodException {
         // TODO 芋艿:临时,方便大家调试
-        if (true) {
+        //if (true) {
+        //    return;
+        //}
+        KeyValue<Long, Integer> bizIdAndBizType = getBizIdAndBizType(joinPoint, crmPermission);
+        Integer bizType = bizIdAndBizType.getValue(); // 模块类型
+        Long bizId = bizIdAndBizType.getKey(); // 模块数据编号
+        Integer permissionLevel = crmPermission.level().getLevel(); // 需要的权限级别
+
+        // TODO 如果是超级管理员则直接通过
+        //if (superAdmin){
+        //    return;
+        //}
+
+        // 1. 获取数据权限
+        List<CrmPermissionDO> bizPermissions = crmPermissionService.getPermissionByBizTypeAndBizId(bizType, bizId);
+        // 2.1 情况一:如果自己是负责人,则默认有所有权限
+        // TODO @puhui999:会不会存在空指针的问题?
+        CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, item -> ObjUtil.equal(item.getUserId(), getUserId()));
+        if (CrmPermissionLevelEnum.isOwner(userPermission.getLevel())) {
             return;
         }
-        try {
-            Long bizId = crmPermission.getIdFor().length > 0 ? getBizId(joinPoint, crmPermission) : (Long) joinPoint.getArgs()[0]; // 获取操作数据的编号
-            Integer bizType = crmPermission.bizType().getType(); // 模块类型
-            Integer permissionLevel = crmPermission.permissionLevel().getLevel(); // 需要的权限级别
-
-            // TODO 如果是超级管理员则直接通过
-            //if (superAdmin){
-            //    return;
-            //}
-
-            // 1. 获取数据权限
-            List<CrmPermissionDO> bizPermissions = crmPermissionService.getPermissionByBizTypeAndBizId(bizType, bizId);
-            // TODO puhui999:这种情况下,最好是 CrmPermissionLevelEnum.isOwner
-            // 2.1 情况一:如果自己是负责人,则默认有所有权限
-            // TODO @puhui999:会不会存在空指针的问题?
-            CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, item -> ObjUtil.equal(item.getUserId(), getUserId()));
-            if (isOwner(userPermission.getPermissionLevel())) {
+        // 2.2 情况二:校验自己是否有读权限
+        if (CrmPermissionLevelEnum.isRead(permissionLevel)) {
+            // 如果没有数据权限或没有负责人则表示此记录为公海数据所有人都有只读权限可以领取成为负责人(团队成员领取的)
+            // TODO @puhui999:89 到 92 这块的逻辑,感觉可以不用 @CrmPermission,公海那自己 check 即可;
+            if (CollUtil.isEmpty(bizPermissions) || CollUtil.anyMatch(bizPermissions,
+                    item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID))) { // 详见 CrmPermissionDO.POOL_USER_ID 注释
                 return;
             }
-            // 2.2 情况二:校验自己是否有读权限
-            if (isRead(permissionLevel)) {
-                // 如果没有数据权限或没有负责人则表示此记录为公海数据所有人都有只读权限可以领取成为负责人(团队成员领取的)
-                // TODO @puhui999:89 到 92 这块的逻辑,感觉可以不用 @CrmPermission,公海那自己 check 即可;
-                if (CollUtil.isEmpty(bizPermissions) || CollUtil.anyMatch(bizPermissions,
-                        item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID))) { // 详见 CrmPermissionDO.POOL_USER_ID 注释
-                    return;
-                }
-                if (isRead(userPermission.getPermissionLevel())) { // 校验当前用户是否有读权限
-                    return;
-                }
-                // 如果查询数据的话拥有写权限的也能查询
-                if (isWrite(userPermission.getPermissionLevel())) { // 校验当前用户是否有写权限
-                    return;
-                }
+            if (CrmPermissionLevelEnum.isRead(userPermission.getLevel())) { // 校验当前用户是否有读权限
+                return;
             }
-            // 2.3 情况三:校验自己是否有写权限
-            if (isWrite(permissionLevel)) {
-                if (isWrite(userPermission.getPermissionLevel())) { // 校验当前用户是否有写权限
-                    return;
-                }
+            // 如果查询数据的话拥有写权限的也能查询
+            if (CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限
+                return;
             }
-
-            // 3. 没通过结束,报错 {} 操作失败,原因:没有权限
-            // TODO @puhui999:这里打个 info 日志,方便后续排查问题、审计;
-            throw exception(CRM_PERMISSION_DENIED, crmPermission.bizType().getName());
-        } catch (Exception ex) {
-            // TODO @puhui999:不用 catch 掉,就是系统异常;
-            log.error("[doBefore][crmPermission({}) 数据校验错误]", toJsonString(crmPermission), ex);
-            // TODO 报错抛个什么异常好呢
-            throw exception(CRM_PERMISSION_DENIED, crmPermission.bizType().getName());
         }
+        // 2.3 情况三:校验自己是否有写权限
+        if (CrmPermissionLevelEnum.isWrite(permissionLevel)) {
+            if (CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限
+                return;
+            }
+        }
+
+        // 打个 info 日志,方便后续排查问题、审计;
+        log.info("[doBefore][crmPermission({}) 数据校验错误]", toJsonString(userPermission));
+        throw exception(CRM_PERMISSION_DENIED, crmPermission.bizType().getName());
+    }
+
+    private KeyValue<Long, Integer> getBizIdAndBizType(JoinPoint joinPoint, CrmPermission crmPermission) throws NoSuchMethodException {
+        Method method = getMethod(joinPoint);
+        // 1. 获取方法的参数值
+        Object[] args = joinPoint.getArgs();
+        EvaluationContext context = bindParam(method, args);
+
+        // 2. 根据spel表达式获取值
+        KeyValue<Long, Integer> keyValue = new KeyValue<>();
+        // 2.1 获取模块数据编号
+        Expression expression = parser.parseExpression(crmPermission.bizId());
+        keyValue.setKey(expression.getValue(context, Long.class));
+        // 2.2 获取模块类型
+        if (ObjUtil.equal(crmPermission.bizType().getType(), CrmBizTypeEnum.CRM_PERMISSION.getType())) {
+            // 情况一:用于 CrmPermissionController 中数据权限校验
+            Expression expression2 = parser.parseExpression(crmPermission.bizTypeValue());
+            keyValue.setValue(expression2.getValue(context, Integer.class));
+            return keyValue;
+        }
+        // 情况二:正常数据权限校验
+        keyValue.setValue(crmPermission.bizType().getType());
+        return keyValue;
+    }
+
+    /**
+     * 获取当前执行的方法
+     */
+    private Method getMethod(JoinPoint joinPoint) throws NoSuchMethodException {
+        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+        Method method = methodSignature.getMethod();
+        return joinPoint.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());
+    }
+
+    /**
+     * 将方法的参数名和参数值绑定
+     *
+     * @param method 方法,根据方法获取参数名
+     * @param args   方法的参数值
+     * @return 求值上下文
+     */
+    private EvaluationContext bindParam(Method method, Object[] args) {
+        //获取方法的参数名
+        String[] params = discoverer.getParameterNames(method);
+
+        //将参数名与参数值对应起来
+        EvaluationContext context = new StandardEvaluationContext();
+        if (params != null) {
+            for (int len = 0; len < params.length; len++) {
+                context.setVariable(params[len], args[len]);
+            }
+        }
+        return context;
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/config/SpelConfig.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/config/SpelConfig.java
new file mode 100644
index 000000000..2bdfcbe8b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/config/SpelConfig.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.framework.core.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+
+/**
+ * 注册 Spel 所需 Bean
+ *
+ * @author HUIHUI
+ */
+@Configuration
+public class SpelConfig {
+
+    @Bean
+    public SpelExpressionParser spelExpressionParser() {
+        return new SpelExpressionParser();
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/service/CrmPermissionValidateService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/service/CrmPermissionValidateService.java
deleted file mode 100644
index 7112d95b5..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/service/CrmPermissionValidateService.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package cn.iocoder.yudao.module.crm.framework.core.service;
-
-/**
- * 校验数据是否存在 service 接口
- * TODO 需要使用团队成员相关操作的业务接口都需要继承此接口
- *
- * @author HUIHUI
- */
-public interface CrmPermissionValidateService {
-
-    /**
-     * 校验数据是否存在
-     *
-     * @param bizType CRM 类型
-     * @param bizId   数据编号
-     * @return 是/否
-     */
-    boolean validateBizIdExists(Integer bizType, Long bizId);
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
index c805615cd..553121243 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.framework.enums;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.Getter;
@@ -16,6 +17,7 @@ import java.util.Arrays;
 @Getter
 public enum CrmBizTypeEnum implements IntArrayValuable {
 
+    CRM_PERMISSION(0, "团队"), // CrmPermissionController 中使用
     CRM_LEADS(1, "线索"),
     CRM_CUSTOMER(2, "客户"),
     CRM_CONTACTS(3, "联系人"),
@@ -33,13 +35,9 @@ public enum CrmBizTypeEnum implements IntArrayValuable {
     private final String name;
 
     public static String getNameByType(Integer type) {
-        // TODO @puhui999:可以 findone,更简洁;另外,不存在返回 null 即可啦;
-        for (CrmBizTypeEnum crmBizTypeEnum : CrmBizTypeEnum.values()) {
-            if (ObjUtil.equal(crmBizTypeEnum.type, type)) {
-                return crmBizTypeEnum.name;
-            }
-        }
-        return "";
+        CrmBizTypeEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(CrmBizTypeEnum.values()),
+                item -> ObjUtil.equal(item.type, type));
+        return typeEnum == null ? null : typeEnum.getName();
     }
 
     @Override
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
index 6eeddc78b..ff09f766f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmPermissionLevelEnum.java
@@ -48,14 +48,4 @@ public enum CrmPermissionLevelEnum implements IntArrayValuable {
         return ObjUtil.equal(WRITE.level, level);
     }
 
-    public static String getNameByLevel(Integer level) {
-        // TODO @puhui999:可以 findone,更简洁;另外,不存在返回 null 即可啦;
-        for (CrmPermissionLevelEnum levelEnum : CrmPermissionLevelEnum.values()) {
-            if (ObjUtil.equal(levelEnum.level, level)) {
-                return levelEnum.name;
-            }
-        }
-        return "";
-    }
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index c20aadcf5..ec5dbe1d4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.crm.service.business;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.framework.core.service.CrmPermissionValidateService;
 
 import javax.validation.Valid;
 import java.util.Collection;
@@ -14,7 +13,7 @@ import java.util.List;
  *
  * @author ljlleo
  */
-public interface CrmBusinessService extends CrmPermissionValidateService {
+public interface CrmBusinessService {
 
     /**
      * 创建商机
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index 20b844ced..d19d8de46 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.crm.service.business;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
-import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
@@ -51,7 +50,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
         // 创建数据权限
         crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType())
-                .setBizId(business.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+                .setBizId(business.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
 
         // 返回
         return business.getId();
@@ -59,8 +58,8 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, getIdFor = CrmBusinessUpdateReqVO.class,
-            permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, bizId = "#updateReqVO.id",
+            level = CrmPermissionLevelEnum.WRITE)
     public void updateBusiness(CrmBusinessUpdateReqVO updateReqVO) {
         // 校验存在
         validateBusinessExists(updateReqVO.getId());
@@ -71,7 +70,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, level = CrmPermissionLevelEnum.WRITE)
     public void deleteBusiness(Long id) {
         // 校验存在
         validateBusinessExists(id);
@@ -88,7 +87,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     }
 
     @Override
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, permissionLevel = CrmPermissionLevelEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, level = CrmPermissionLevelEnum.READ)
     public CrmBusinessDO getBusiness(Long id) {
         return businessMapper.selectById(id);
     }
@@ -134,14 +133,4 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         // 3. TODO 记录转移日志
     }
 
-    @Override
-    public boolean validateBizIdExists(Integer bizType, Long bizId) {
-        // 1. 校验模块类型
-        if (!ObjUtil.equal(CrmBizTypeEnum.CRM_BUSINESS.getType(), bizId)) {
-            return false;
-        }
-        // 2. 校验是否存在
-        return businessMapper.selectById(bizId) != null;
-    }
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index dafebb598..790229c6b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -47,7 +47,7 @@ public class ContactServiceImpl implements ContactService {
 
         // 创建数据权限
         crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CONTACTS.getType())
-                .setBizId(contact.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+                .setBizId(contact.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
 
         // 返回
         return contact.getId();
@@ -55,8 +55,7 @@ public class ContactServiceImpl implements ContactService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, getIdFor = ContactUpdateReqVO.class,
-            permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, level = CrmPermissionLevelEnum.WRITE)
     public void updateContact(ContactUpdateReqVO updateReqVO) {
         // 校验存在
         validateContactExists(updateReqVO.getId());
@@ -69,7 +68,7 @@ public class ContactServiceImpl implements ContactService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, level = CrmPermissionLevelEnum.WRITE)
     public void deleteContact(Long id) {
         // 校验存在
         validateContactExists(id);
@@ -86,7 +85,7 @@ public class ContactServiceImpl implements ContactService {
     }
 
     @Override
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, permissionLevel = CrmPermissionLevelEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, level = CrmPermissionLevelEnum.READ)
     public ContactDO getContact(Long id) {
         return contactMapper.selectById(id);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
index 523470cd8..393487d97 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImpl.java
@@ -46,7 +46,7 @@ public class ContractServiceImpl implements ContractService {
 
         // 创建数据权限
         crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType())
-                .setBizId(contract.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+                .setBizId(contract.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
 
         // 返回
         return contract.getId();
@@ -54,8 +54,7 @@ public class ContractServiceImpl implements ContractService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, getIdFor = ContractUpdateReqVO.class,
-            permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, level = CrmPermissionLevelEnum.WRITE)
     public void updateContract(ContractUpdateReqVO updateReqVO) {
         // 校验存在
         validateContractExists(updateReqVO.getId());
@@ -66,7 +65,7 @@ public class ContractServiceImpl implements ContractService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, level = CrmPermissionLevelEnum.WRITE)
     public void deleteContract(Long id) {
         // 校验存在
         validateContractExists(id);
@@ -83,7 +82,7 @@ public class ContractServiceImpl implements ContractService {
     }
 
     @Override
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, permissionLevel = CrmPermissionLevelEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, level = CrmPermissionLevelEnum.READ)
     public ContractDO getContract(Long id) {
         return contractMapper.selectById(id);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index e4fafd5cc..2580bc854 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -1,7 +1,5 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.collection.ListUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
@@ -12,13 +10,11 @@ import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 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.dept.DeptApi;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 
@@ -47,7 +43,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
         // 创建数据权限
         crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
-                .setBizId(customer.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+                .setBizId(customer.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
 
         // 返回
         return customer.getId();
@@ -55,8 +51,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, getIdFor = CrmCustomerUpdateReqVO.class,
-            permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, level = CrmPermissionLevelEnum.WRITE)
     public void updateCustomer(CrmCustomerUpdateReqVO updateReqVO) {
         // 校验存在
         validateCustomerExists(updateReqVO.getId());
@@ -69,7 +64,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, permissionLevel = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, level = CrmPermissionLevelEnum.WRITE)
     public void deleteCustomer(Long id) {
         // 校验存在
         validateCustomerExists(id);
@@ -86,7 +81,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     @Override
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, permissionLevel = CrmPermissionLevelEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, level = CrmPermissionLevelEnum.READ)
     public CrmCustomerDO getCustomer(Long id) {
         return customerMapper.selectById(id);
     }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index 59d160773..18c7beffc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -6,8 +6,8 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 
 import javax.validation.Valid;
 import java.util.List;
@@ -63,9 +63,9 @@ public interface CrmPermissionService {
     /**
      * 数据权限转移
      *
-     * @param crmTransferPermissionReqBO 数据权限转移请求
+     * @param crmPermissionTransferReqBO 数据权限转移请求
      */
-    void transferPermission(@Valid CrmTransferPermissionReqBO crmTransferPermissionReqBO);
+    void transferPermission(@Valid CrmPermissionTransferReqBO crmPermissionTransferReqBO);
 
     /**
      * 获取数据权限分页数据
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index 527d8edc6..4265545a4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -10,8 +10,8 @@ import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -55,13 +55,12 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updatePermission(CrmPermissionUpdateReqBO updateBO) {
-        // TODO @puhui999:这里 1.1 1.2;下面 2.;这样更有序一点;
-        // 1. 校验用户是否存在
+        // 1.1 校验用户是否存在
         adminUserApi.validateUserList(Collections.singletonList(updateBO.getUserId()));
-        // 2. 校验存在
+        // 1.2 校验存在
         validateCrmPermissionExists(updateBO.getId());
 
-        // 更新操作
+        // 2. 更新操作
         CrmPermissionDO updateDO = CrmPermissionConvert.INSTANCE.convert(updateBO);
         crmPermissionMapper.updateById(updateDO);
     }
@@ -93,43 +92,41 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     }
 
     @Override
-    public void transferPermission(CrmTransferPermissionReqBO transferReqBO) {
+    public void transferPermission(CrmPermissionTransferReqBO transferReqBO) {
         // 1. 校验数据权限-是否是负责人,只有负责人才可以转移
         CrmPermissionDO oldPermission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(transferReqBO.getBizType(),
                 transferReqBO.getBizId(), transferReqBO.getUserId());
         String crmName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType());
         // TODO 校验是否为超级管理员 || 1
-        if (oldPermission == null || !isOwner(oldPermission.getPermissionLevel())) {
+        if (oldPermission == null || !isOwner(oldPermission.getLevel())) {
             throw exception(CRM_PERMISSION_DENIED, crmName);
         }
-
-        // TODO @puhui999:这个顺序编号,看看调整下;2. 后面是 2.1 ,结果没 2.2 有点怪;
-        // 2. 校验转移对象是否已经是该负责人
+        // 1.1 校验转移对象是否已经是该负责人
         if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) {
             throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, crmName);
         }
-        // 2.1 校验新负责人是否存在
+        // 1.2 校验新负责人是否存在
         adminUserApi.validateUserList(Collections.singletonList(transferReqBO.getNewOwnerUserId()));
-        // 3. 权限转移
+
+        // 2. 权限转移
         List<CrmPermissionDO> permissions = crmPermissionMapper.selectByBizTypeAndBizId(
                 transferReqBO.getBizType(), transferReqBO.getBizId()); // 获取所有团队成员
-        // 3.1 校验新负责人是否在团队成员中
+        // 2.1 校验新负责人是否在团队成员中
         CrmPermissionDO permission = CollUtil.findOne(permissions,
                 item -> ObjUtil.equal(item.getUserId(), transferReqBO.getNewOwnerUserId()));
         if (permission == null) { // 不存在则以负责人的级别加入这个团队
             crmPermissionMapper.insert(new CrmPermissionDO().setBizType(transferReqBO.getBizType())
                     .setBizId(transferReqBO.getBizId()).setUserId(transferReqBO.getNewOwnerUserId())
-                    .setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+                    .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
 
         } else { // 存在则修改权限级别
             crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId())
-                    .setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+                    .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
         }
-
-        // 4. 老负责人处理
-        if (transferReqBO.getJoinTeam()) { // 加入团队
+        // 2.2. 老负责人处理
+        if (transferReqBO.getOldOwnerPermissionLevel() != null) { // 加入团队
             crmPermissionMapper.updateById(new CrmPermissionDO().setId(oldPermission.getId())
-                    .setPermissionLevel(transferReqBO.getPermissionLevel())); // 设置加入团队后的级别
+                    .setLevel(transferReqBO.getOldOwnerPermissionLevel())); // 设置加入团队后的级别
             return;
         }
         crmPermissionMapper.deleteById(oldPermission.getId()); // 移除
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
index fac0ef288..fce59cac1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionCreateReqBO.java
@@ -33,12 +33,11 @@ public class CrmPermissionCreateReqBO {
     @NotNull(message = "Crm 数据编号不能为空")
     private Long bizId;
 
-    // TODO @puhui999:简化成 level
     /**
      * 权限级别
      */
     @NotNull(message = "权限级别不能为空")
     @InEnum(CrmPermissionLevelEnum.class)
-    private Integer permissionLevel;
+    private Integer level;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java
similarity index 64%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java
index d4cbdb43c..23da43daf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmTransferPermissionReqBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java
@@ -7,14 +7,13 @@ import lombok.Data;
 
 import javax.validation.constraints.NotNull;
 
-// TODO @puhui999:CrmPermissionTransferReqBO
 /**
  * 数据权限转移 Request BO
  *
  * @author HUIHUI
  */
 @Data
-public class CrmTransferPermissionReqBO {
+public class CrmPermissionTransferReqBO {
 
     /**
      * 当前登录用户编号
@@ -40,17 +39,10 @@ public class CrmTransferPermissionReqBO {
     @NotNull(message = "新负责人的用户编号不能为空")
     private Long newOwnerUserId;
 
-    // TODO @puhui999:joinTeam 可以合并成 permissionLevel 里;oldOwnerPermissionLevel;这样 null 说明移除,因为都换负责人啦;
     /**
-     * 老负责人是否加入团队,是/否
-     */
-    @NotNull(message = "老负责人是否加入团队不能为空")
-    private Boolean joinTeam;
-
-    /**
-     * 老负责人加入团队后的权限级别。如果 {@link #joinTeam} 为 false, permissionLevel 为 null
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
      * 关联 {@link CrmPermissionLevelEnum}
      */
-    private Integer permissionLevel;
+    private Integer oldOwnerPermissionLevel;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
index d3922a4d3..38eff3ef5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
@@ -1,8 +1,6 @@
 package cn.iocoder.yudao.module.crm.service.permission.bo;
 
 import cn.iocoder.yudao.framework.common.validation.InEnum;
-import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import lombok.Data;
 
@@ -28,25 +26,11 @@ public class CrmPermissionUpdateReqBO {
     @NotNull(message = "用户编号不能为空")
     private Long userId;
 
-    // TODO @puhui999:id 字段,和 bizType + bizId 是否二选一;
-    /**
-     * Crm 类型
-     */
-    @NotNull(message = "Crm 类型不能为空")
-    @InEnum(CrmBizTypeEnum.class)
-    private Integer bizType;
-    /**
-     * 数据编号
-     */
-    @NotNull(message = "Crm 数据编号不能为空")
-    private Long bizId;
-
-    // TODO @puhui999:简化成 level
     /**
      * 权限级别
      */
     @NotNull(message = "权限级别不能为空")
     @InEnum(CrmPermissionLevelEnum.class)
-    private Integer permissionLevel;
+    private Integer level;
 
 }
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java
index 57db07cc2..88709209b 100644
--- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApi.java
@@ -1,6 +1,11 @@
 package cn.iocoder.yudao.module.system.api.dept;
 
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
+
 import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 岗位 API 接口
@@ -18,4 +23,11 @@ public interface PostApi {
      */
     void validPostList(Collection<Long> ids);
 
+    List<PostRespDTO> getPostList(Collection<Long> ids);
+
+    default Map<Long, PostRespDTO> getPostMap(Collection<Long> ids) {
+        List<PostRespDTO> list = getPostList(ids);
+        return CollectionUtils.convertMap(list, PostRespDTO::getId);
+    }
+
 }
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/dto/PostRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/dto/PostRespDTO.java
new file mode 100644
index 000000000..cf2cc2543
--- /dev/null
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/dto/PostRespDTO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.system.api.dept.dto;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import lombok.Data;
+
+/**
+ * 岗位 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class PostRespDTO {
+
+    /**
+     * 岗位序号
+     */
+    private Long id;
+    /**
+     * 岗位名称
+     */
+    private String name;
+    /**
+     * 岗位编码
+     */
+    private String code;
+    /**
+     * 岗位排序
+     */
+    private Integer sort;
+    /**
+     * 状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+
+}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java
index 3d8cdf997..3e19395d5 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java
@@ -1,10 +1,13 @@
 package cn.iocoder.yudao.module.system.api.dept;
 
+import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
+import cn.iocoder.yudao.module.system.convert.dept.PostConvert;
 import cn.iocoder.yudao.module.system.service.dept.PostService;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.util.Collection;
+import java.util.List;
 
 /**
  * 岗位 API 实现类
@@ -22,4 +25,9 @@ public class PostApiImpl implements PostApi {
         postService.validatePostList(ids);
     }
 
+    @Override
+    public List<PostRespDTO> getPostList(Collection<Long> ids) {
+        return PostConvert.INSTANCE.convert(postService.getPostList(ids));
+    }
+
 }
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/PostConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/PostConvert.java
index 86a548edb..15a8599ca 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/PostConvert.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/dept/PostConvert.java
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.system.convert.dept;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.*;
 import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
 import org.mapstruct.Mapper;
@@ -25,4 +26,6 @@ public interface PostConvert {
 
     List<PostExcelVO> convertList03(List<PostDO> list);
 
+    List<PostRespDTO> convert(List<PostDO> postList);
+
 }

From 15313c2992258c248433b73b6d921189ad775d4f Mon Sep 17 00:00:00 2001
From: jason <2667446@qq.com>
Date: Tue, 7 Nov 2023 23:11:07 +0800
Subject: [PATCH 072/101] =?UTF-8?q?=E8=BD=AC=E8=B4=A6=20-=20=E5=A2=9E?=
 =?UTF-8?q?=E5=8A=A0=E8=BD=AC=E8=B4=A6=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../framework/pay/core/client/PayClient.java  |   9 +
 .../dto/transfer/PayTransferRespDTO.java      |  17 +-
 .../core/client/impl/AbstractPayClient.java   |  22 ++-
 .../impl/alipay/AbstractAlipayPayClient.java  | 108 ++++++++----
 .../core/client/impl/mock/MockPayClient.java  |   6 +
 .../impl/weixin/AbstractWxPayClient.java      |   7 +
 .../transfer/PayTransferStatusRespEnum.java   |   4 +
 .../module/pay/enums/ErrorCodeConstants.java  |   6 +-
 .../enums/transfer/PayTransferStatusEnum.java |   4 +
 .../convert/transfer/PayTransferConvert.java  |   2 +-
 .../dal/mysql/transfer/PayTransferMapper.java |   6 +-
 .../framework/pay/core/WalletPayClient.java   |   6 +
 .../pay/job/transfer/PayTransferSyncJob.java  |  30 ++++
 .../service/transfer/PayTransferService.java  |   6 +
 .../transfer/PayTransferServiceImpl.java      | 164 +++++++++++++-----
 15 files changed, 314 insertions(+), 83 deletions(-)
 create mode 100644 yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java

diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
index 18ae017d1..86e3566b2 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 
 import java.util.Map;
 
@@ -86,4 +87,12 @@ public interface PayClient {
      */
     PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
 
+    /**
+     * 获得转账订单信息
+     *
+     * @param outTradeNo 外部订单号
+     * @param type 转账类型
+     * @return 转账信息
+     */
+    PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
 }
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
index da6f22774..0f9b48240 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
@@ -53,11 +53,24 @@ public class PayTransferRespDTO {
     /**
      * 创建【WAITING】状态的转账返回
      */
-    public static PayTransferRespDTO waitingOf(String channelOrderNo,
+    public static PayTransferRespDTO waitingOf(String channelTransferNo,
                                              String outTransferNo, Object rawData) {
         PayTransferRespDTO respDTO = new PayTransferRespDTO();
         respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
-        respDTO.channelTransferNo = channelOrderNo;
+        respDTO.channelTransferNo = channelTransferNo;
+        respDTO.outTransferNo = outTransferNo;
+        respDTO.rawData = rawData;
+        return respDTO;
+    }
+
+    /**
+     * 创建【IN_PROGRESS】状态的转账返回
+     */
+    public static PayTransferRespDTO dealingOf(String channelTransferNo,
+                                               String outTransferNo, Object rawData) {
+        PayTransferRespDTO respDTO = new PayTransferRespDTO();
+        respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
+        respDTO.channelTransferNo = channelTransferNo;
         respDTO.outTransferNo = outTransferNo;
         respDTO.rawData = rawData;
         return respDTO;
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
index f06dab22e..82d68b58f 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
@@ -188,11 +188,11 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 
     @Override
     public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
+        validatePayTransferReqDTO(reqDTO);
         PayTransferRespDTO resp;
-        try{
-            validatePayTransferReqDTO(reqDTO);
+        try {
             resp = doUnifiedTransfer(reqDTO);
-        }catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
             throw ex;
         } catch (Throwable ex) {
             // 系统异常,则包装成 PayException 异常抛出
@@ -219,9 +219,25 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         }
     }
 
+    @Override
+    public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        try {
+            return doGetTransfer(outTradeNo, type);
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+            throw ex;
+        } catch (Throwable ex) {
+            log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
+                    getId(), outTradeNo, type, ex);
+            throw buildPayException(ex);
+        }
+    }
+
     protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
             throws Throwable;
 
+    protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
+            throws Throwable;
+
     // ========== 各种工具方法 ==========
 
     private PayException buildPayException(Throwable ex) {
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
index fc9d658ac..4dcf23675 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
@@ -23,14 +23,8 @@ import com.alipay.api.AlipayResponse;
 import com.alipay.api.DefaultAlipayClient;
 import com.alipay.api.domain.*;
 import com.alipay.api.internal.util.AlipaySignature;
-import com.alipay.api.request.AlipayFundTransUniTransferRequest;
-import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
-import com.alipay.api.request.AlipayTradeQueryRequest;
-import com.alipay.api.request.AlipayTradeRefundRequest;
-import com.alipay.api.response.AlipayFundTransUniTransferResponse;
-import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
-import com.alipay.api.response.AlipayTradeQueryResponse;
-import com.alipay.api.response.AlipayTradeRefundResponse;
+import com.alipay.api.request.*;
+import com.alipay.api.response.*;
 import lombok.Getter;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -126,7 +120,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         }
         // 2.2 解析订单的状态
         Integer status = parseStatus(response.getTradeStatus());
-        Assert.notNull(status,  () -> {
+        Assert.notNull(status, () -> {
             throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
         });
         return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
@@ -228,7 +222,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
         // 1.1 校验公钥类型 必须使用公钥证书模式
         if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
-            throw exception0(ERROR_CONFIGURATION.getCode(),"支付宝单笔转账必须使用公钥证书模式");
+            throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
         }
         // 1.2 构建 AlipayFundTransUniTransferModel
         AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
@@ -238,45 +232,97 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         model.setOutBizNo(reqDTO.getOutTransferNo());
         model.setProductCode("TRANS_ACCOUNT_NO_PWD");    // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
         model.setBizScene("DIRECT_TRANSFER");           // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
-        model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+        if (reqDTO.getChannelExtras() != null) {
+            model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+        }
+        // ② 个性化的参数
+        Participant payeeInfo = new Participant();
         PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
         switch (transferType) {
             // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
             // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
             case ALIPAY_BALANCE: {
-                // ② 个性化的参数
-                Participant payeeInfo = new Participant();
                 payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
                 payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
                 payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
                 model.setPayeeInfo(payeeInfo);
-                // 1.3 构建 AlipayFundTransUniTransferRequest
-                AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
-                request.setBizModel(model);
-                // 执行请求
-                AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
-                // 处理结果
-                if (!response.isSuccess()) {
-                    // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
-                    if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
-                        return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
-                    }
-                    return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
-                            reqDTO.getOutTransferNo(), response);
-                }
-                return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
-                        response.getOutBizNo(), response);
+                break;
             }
             case BANK_CARD: {
-                Participant payeeInfo = new Participant();
                 payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
                 // TODO 待实现
                 throw exception(NOT_IMPLEMENTED);
             }
             default: {
-                throw exception0(BAD_REQUEST.getCode(),"不正确的转账类型: {}",transferType);
+                throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
             }
         }
+        // 1.3 构建 AlipayFundTransUniTransferRequest
+        AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
+        request.setBizModel(model);
+        // 执行请求
+        AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
+        // 处理结果
+        if (!response.isSuccess()) {
+            // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
+            // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
+            if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+                return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
+            }
+            return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    reqDTO.getOutTransferNo(), response);
+        } else {
+            if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+                return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                        reqDTO.getOutTransferNo(), response);
+            }
+            if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING"  处理中
+                return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
+            }
+            return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
+                    response.getOutBizNo(), response);
+        }
+
+    }
+
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable {
+        // 1.1 构建 AlipayFundTransCommonQueryModel
+        AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
+        model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
+        model.setBizScene("DIRECT_TRANSFER"); //业务场景
+        model.setOutBizNo(outTradeNo);
+        // 1.2 构建 AlipayFundTransCommonQueryRequest
+        AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
+        request.setBizModel(model);
+
+        // 2.1 执行请求
+        AlipayFundTransCommonQueryResponse response;
+        if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
+            response = client.certificateExecute(request);
+        } else {
+            response = client.execute(request);
+        }
+        // 2.2 处理返回结果
+        if (response.isSuccess()) {
+            if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+                return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                        outTradeNo, response);
+            }
+            if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
+                return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
+            }
+            return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
+                    response.getOutBizNo(), response);
+        } else {
+            // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+            // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+            if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+                return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
+            }
+            return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    outTradeNo, response);
+        }
     }
 
     // ========== 各种工具方法 ==========
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
index 309813697..1ad1ad713 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifie
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 
 import java.time.LocalDateTime;
 import java.util.Map;
@@ -71,4 +72,9 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
         throw new UnsupportedOperationException("待实现");
     }
 
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
 }
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
index f4f326a65..bb6feeb04 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
@@ -16,6 +16,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
@@ -431,6 +432,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
        throw new UnsupportedOperationException("待实现");
     }
+
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
     // ========== 各种工具方法 ==========
 
     static String formatDateV2(LocalDateTime time) {
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
index 63b3a96aa..35ea344da 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
@@ -38,4 +38,8 @@ public enum PayTransferStatusRespEnum {
     public static boolean isClosed(Integer status) {
         return Objects.equals(status, CLOSED.getStatus());
     }
+
+    public static boolean isInProgress(Integer status) {
+        return Objects.equals(status, IN_PROGRESS.getStatus());
+    }
 }
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
index 8fc38e61c..8b7a38ecf 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
@@ -66,9 +66,9 @@ public interface ErrorCodeConstants {
     // ========== 转账模块 1-007-009-000 ==========
     ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}");
     ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
-    ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1_007_009_002, "转账单已成功转账");
-    ErrorCode PAY_TRANSFER_EXISTS = new ErrorCode(1_007_009_003, "已经存在转账单");
-    ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经存在,请查询转账订单相关状态");
+    ErrorCode PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH = new ErrorCode(1_007_009_002, "两次相同转账请求的类型不匹配");
+    ErrorCode PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "两次相同转账请求的金额不匹配");
+    ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经发起,请查询转账订单相关状态");
     ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账");
     ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");
 
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
index 335a470f8..6f2f27c75 100644
--- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/transfer/PayTransferStatusEnum.java
@@ -40,9 +40,13 @@ public enum PayTransferStatusEnum {
     public static boolean isClosed(Integer status) {
         return Objects.equals(status, CLOSED.getStatus());
     }
+
     public static boolean isWaiting(Integer status) {
         return Objects.equals(status, WAITING.getStatus());
     }
+    public static boolean isInProgress(Integer status) {
+        return Objects.equals(status, IN_PROGRESS.getStatus());
+    }
 
     /**
      * 是否处于待转账或者转账中的状态
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
index 4d5849ddd..4e79548d0 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/transfer/PayTransferConvert.java
@@ -18,7 +18,7 @@ public interface PayTransferConvert {
 
     PayTransferDO convert(PayTransferCreateReqDTO dto);
 
-    PayTransferUnifiedReqDTO convert2(PayTransferCreateReqDTO dto);
+    PayTransferUnifiedReqDTO convert2(PayTransferDO dto);
 
     PayTransferCreateReqDTO convert(PayTransferCreateReqVO vo);
 
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
index 0f7384526..af4f6debf 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/transfer/PayTransferMapper.java
@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.pay.dal.mysql.transfer;
 
 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.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -40,6 +40,10 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
                 .betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime())
                 .orderByDesc(PayTransferDO::getId));
     }
+
+    default List<PayTransferDO> selectListByStatus(Integer status){
+        return selectList(PayTransferDO::getStatus, status);
+    }
 }
 
 
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
index c1b72ef9e..efd3d0ba1 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
@@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
@@ -181,4 +182,9 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
         throw new UnsupportedOperationException("待实现");
     }
 
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java
new file mode 100644
index 000000000..191071e10
--- /dev/null
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/transfer/PayTransferSyncJob.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.pay.job.transfer;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.pay.service.transfer.PayTransferService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 转账订单的同步 Job
+ *
+ * 由于转账订单的转账结果,有些渠道是异步通知进行同步的,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
+ *
+ * @author jason
+ */
+@Component
+public class PayTransferSyncJob implements JobHandler {
+
+    @Resource
+    private PayTransferService transferService;
+
+    @Override
+    @TenantJob
+    public String execute(String param) {
+        int count = transferService.syncTransfer();
+        return StrUtil.format("同步转账订单 {} 个", count);
+    }
+}
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
index 0848dc0ca..9a58cf06a 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferService.java
@@ -48,4 +48,10 @@ public interface PayTransferService {
      */
     PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO);
 
+    /**
+     * 同步渠道转账单状态
+     *
+     * @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的
+     */
+    int syncTransfer();
 }
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
index 27b9807bd..73b726dcd 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
+++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
@@ -1,14 +1,16 @@
 package cn.iocoder.yudao.module.pay.service.transfer;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.extra.spring.SpringUtil;
-import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
@@ -18,6 +20,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper;
 import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
+import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -27,7 +30,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import javax.validation.Validator;
-import java.util.Objects;
+import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert.INSTANCE;
@@ -75,56 +78,60 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     @Override
     public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
-        // 1.1 校验转账单是否可以提交
-        validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId());
-        // 1.2 校验 App
+        // 1.1 校验 App
         PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
-        // 1.3 校验支付渠道是否有效
+        // 1.2 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
         PayClient client = channelService.getPayClient(channel.getId());
         if (client == null) {
             log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
             throw exception(CHANNEL_NOT_FOUND);
         }
-        // 2.创建转账单
-        String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
-        PayTransferDO transfer = INSTANCE.convert(reqDTO)
-                .setChannelId(channel.getId())
-                .setNo(no).setStatus(WAITING.getStatus())
-                .setNotifyUrl(payApp.getTransferNotifyUrl());
-        transferMapper.insert(transfer);
-        PayTransferRespDTO unifiedTransferResp = null;
+        // 1.3 校验转账单已经发起过转账。
+        PayTransferDO transfer = validateTransferCanCreate(reqDTO);
+
+        if (transfer == null) {
+            // 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
+            String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
+            transfer = INSTANCE.convert(reqDTO)
+                    .setChannelId(channel.getId())
+                    .setNo(no).setStatus(WAITING.getStatus())
+                    .setNotifyUrl(payApp.getTransferNotifyUrl());
+            transferMapper.insert(transfer);
+        }
         try {
             // 3. 调用三方渠道发起转账
-            PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(reqDTO)
-                    .setOutTransferNo(no);
-            unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
-        } catch (ServiceException ex) {
-            // 业务异常.直接返回转账失败的结果
-            log.error("[createTransfer][转账 id({}) requestDTO({}) 发生业务异常]", transfer.getId(), reqDTO, ex);
-            unifiedTransferResp = PayTransferRespDTO.closedOf("", "", no, ex);
-        } catch (Throwable e) {
-            // 注意这里仅打印异常,不进行抛出。
-            // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续通过转账回调、或者转账轮询可以拿到。
-            // TODO 需要加转账回调业务接口 和 转账轮询未实现
-            // 最终,在异常的情况下,支付中心会异步回调业务的转账回调接口,提供转账结果
-            log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
-        }
-        if (Objects.nonNull(unifiedTransferResp)) {
+            PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
+                    .setOutTransferNo(transfer.getNo());
+            PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
             // 4. 通知转账结果
             getSelf().notifyTransfer(channel, unifiedTransferResp);
+        } catch (Throwable e) {
+            // 注意这里仅打印异常,不进行抛出。
+            // 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续转账轮询可以拿到。
+            // 或者使用相同 no 再次发起转账请求
+            log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
         }
+
         return transfer.getId();
     }
 
-    @Override
-    public PayTransferDO getTransfer(Long id) {
-        return transferMapper.selectById(id);
-    }
-
-    @Override
-    public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
-        return transferMapper.selectPage(pageReqVO);
+    private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) {
+        PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId());
+        if (transfer != null) {
+            // 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果.
+            if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
+                throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
+            }
+            if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) {
+                throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH);
+            }
+            if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) {
+                throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH);
+            }
+        }
+        // 如果状态为等待状态。不知道渠道转账是否发起成功。 允许使用相同的 no 再次发起转账,渠道会保证幂等
+        return transfer;
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -138,17 +145,40 @@ public class PayTransferServiceImpl implements PayTransferService {
         if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) {
             notifyTransferClosed(channel, notify);
         }
+        // 转账处理中的回调
+        if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) {
+            notifyTransferInProgress(channel, notify);
+        }
         // WAITING 状态无需处理
-        // TODO IN_PROGRESS 待处理
     }
 
-    private void validateTransferCanCreate(Long appId, String merchantTransferId) {
-        PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, merchantTransferId);
-        if (transfer != null) {  // 是否存在
-            throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
+    private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
+        // 1.校验
+        PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
+        if (transfer == null) {
+            throw exception(PAY_TRANSFER_NOT_FOUND);
         }
+        if (isInProgress(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新
+            return;
+        }
+        if (!isWaiting(transfer.getStatus())) {
+            throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
+        }
+        // 2.更新
+        int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
+                CollUtil.newArrayList(WAITING.getStatus()),
+                new PayTransferDO().setStatus(IN_PROGRESS.getStatus()));
+        if (updateCounts == 0) {
+            throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
+        }
+        log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId());
+
+        // 3. 插入转账通知记录
+        notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
+                transfer.getId());
     }
 
+
     private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
         // 1.校验
         PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
@@ -210,6 +240,56 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     }
 
+    @Override
+    public PayTransferDO getTransfer(Long id) {
+        return transferMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
+        return transferMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public int syncTransfer() {
+        List<PayTransferDO> list = transferMapper.selectListByStatus(WAITING.getStatus());
+        if (CollUtil.isEmpty(list)) {
+            return 0;
+        }
+        int count = 0;
+        for (PayTransferDO transfer : list) {
+            count += syncTransfer(transfer) ? 1 : 0;
+        }
+        return count;
+    }
+
+    private boolean syncTransfer(PayTransferDO transfer) {
+        try {
+            // 1. 查询转账订单信息
+            PayClient payClient = channelService.getPayClient(transfer.getChannelId());
+            if (payClient == null) {
+                log.error("[syncTransfer][渠道编号({}) 找不到对应的支付客户端]", transfer.getChannelId());
+                return false;
+            }
+            PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo(),
+                    PayTransferTypeEnum.typeOf(transfer.getType()));
+
+            // 2. 回调转账结果
+            notifyTransfer(transfer.getChannelId(), resp);
+            return true;
+        } catch (Throwable ex) {
+            log.error("[syncTransfer][transfer({}) 同步转账单状态异常]", transfer.getId(), ex);
+            return false;
+        }
+    }
+
+    private void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
+        // 校验渠道是否有效
+        PayChannelDO channel = channelService.validPayChannel(channelId);
+        // 通知转账结果给对应的业务
+        TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyTransfer(channel, notify));
+    }
+
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *

From 71574da2cf0319fb47f39443e4cd2cc82fd4a3ed Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Wed, 8 Nov 2023 00:42:46 +0800
Subject: [PATCH 073/101] =?UTF-8?q?feat:=20CRM=20=E5=AE=A2=E6=88=B7?=
 =?UTF-8?q?=E5=85=AC=E6=B5=B7=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/customer/CrmCustomerController.http |  4 --
 .../CrmCustomerPoolConfigController.java      | 44 ++++++++++++++++
 .../vo/CrmCustomerPoolConfigBaseVO.java       | 30 +++++++++++
 .../vo/CrmCustomerPoolConfigRespVO.java       | 14 +++++
 .../vo/CrmCustomerPoolConfigUpdateReqVO.java  | 14 +++++
 .../convert/customer/CrmCustomerConvert.java  |  5 ++
 .../dataobject/customer/CrmCustomerDO.java    |  2 +-
 .../customer/CrmCustomerPoolConfigDO.java     | 49 ++++++++++++++++++
 .../customer/CrmCustomerPoolConfigMapper.java | 14 +++++
 .../CrmCustomerPoolConfigService.java         | 28 ++++++++++
 .../CrmCustomerPoolConfigServiceImpl.java     | 51 +++++++++++++++++++
 .../customer/CrmCustomerServiceImpl.java      |  4 --
 .../customer/CrmCustomerPoolConfigMapper.xml  | 12 +++++
 13 files changed, 262 insertions(+), 9 deletions(-)
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerPoolConfigDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerPoolConfigMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerPoolConfigMapper.xml

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http
deleted file mode 100644
index 1dfa2c691..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http
+++ /dev/null
@@ -1,4 +0,0 @@
-### 请求 /crm/customer/test 接口 => 成功
-GET {{baseUrl}}/crm/customer/test
-tenant-id: 1
-Authorization: Bearer {{token}}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
new file mode 100644
index 000000000..fb2325fac
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPoolConfigRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPoolConfigUpdateReqVO;
+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;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - CRM 客户公海配置")
+@RestController
+@RequestMapping("/crm/customer-pool-config")
+@Validated
+public class CrmCustomerPoolConfigController {
+
+    @Resource
+    private CrmCustomerPoolConfigService customerPoolConfigService;
+
+    @GetMapping("/get")
+    @Operation(summary = "获取客户公海规则设置")
+    @PreAuthorize("@ss.hasPermission('crm:customer-pool-config:query')")
+    public CommonResult<CrmCustomerPoolConfigRespVO> getCustomerPoolConfig() {
+        CrmCustomerPoolConfigDO customerPoolConfig = customerPoolConfigService.getCustomerPoolConfig();
+        return success(CrmCustomerConvert.INSTANCE.convert(customerPoolConfig));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新客户公海规则设置")
+    @PreAuthorize("@ss.hasPermission('crm:customer-pool-config:update')")
+    public CommonResult<Boolean> updateCustomerPoolConfig(@Valid @RequestBody CrmCustomerPoolConfigUpdateReqVO updateReqVO) {
+        customerPoolConfigService.updateCustomerPoolConfig(updateReqVO);
+        return success(true);
+    }
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigBaseVO.java
new file mode 100644
index 000000000..60e6c1b3c
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigBaseVO.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 客户公海配置 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmCustomerPoolConfigBaseVO {
+
+    @Schema(description = "是否启用客户公海", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否启用客户公海不能为空")
+    private Boolean enabled;
+
+    @Schema(description = "未跟进放入公海天数", example = "2")
+    private Integer contactExpireDays;
+
+    @Schema(description = "未成交放入公海天数", example = "2")
+    private Integer dealExpireDays;
+
+    @Schema(description = "是否开启提前提醒", example = "true")
+    private Boolean notifyEnabled;
+
+    @Schema(description = "提前提醒天数", example = "2")
+    private Integer notifyDays;
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigRespVO.java
new file mode 100644
index 000000000..965a337b0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigRespVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 客户公海规则 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerPoolConfigRespVO extends CrmCustomerPoolConfigBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigUpdateReqVO.java
new file mode 100644
index 000000000..ba72a6b23
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigUpdateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 客户更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerPoolConfigUpdateReqVO extends CrmCustomerPoolConfigBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index ffb466208..d1e02ae91 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -1,11 +1,13 @@
 package cn.iocoder.yudao.module.crm.convert.customer;
 
+import cn.hutool.core.lang.tree.Node;
 import cn.hutool.core.util.NumberUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 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.CrmTransferPermissionReqBO;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -60,4 +62,7 @@ public interface CrmCustomerConvert {
     })
     CrmTransferPermissionReqBO convert(CrmCustomerTransferReqVO reqVO, Long userId);
 
+    CrmCustomerPoolConfigRespVO convert(CrmCustomerPoolConfigDO customerPoolConfig);
+
+    CrmCustomerPoolConfigDO convert(CrmCustomerPoolConfigUpdateReqVO updateReqVO);
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index 3e00b1cc5..23a786e1f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -15,7 +15,7 @@ import java.time.LocalDateTime;
  *
  * @author Wanwan
  */
-@TableName(value = "crm_customer", autoResultMap = true)
+@TableName(value = "crm_customer")
 @KeySequence("crm_customer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerPoolConfigDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerPoolConfigDO.java
new file mode 100644
index 000000000..cec0a05bc
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerPoolConfigDO.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.customer;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 客户公海配置 DO
+ *
+ * @author Wanwan
+ */
+@TableName(value = "crm_customer_pool_config")
+@KeySequence("crm_customer_pool_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmCustomerPoolConfigDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 是否启用客户公海
+     */
+    private Boolean enabled;
+    /**
+     * 未跟进放入公海天数
+     */
+    private Integer contactExpireDays;
+    /**
+     * 未成交放入公海天数
+     */
+    private Integer dealExpireDays;
+    /**
+     * 是否开启提前提醒
+     */
+    private Boolean notifyEnabled;
+    /**
+     * 提前提醒天数
+     */
+    private Integer notifyDays;
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerPoolConfigMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerPoolConfigMapper.java
new file mode 100644
index 000000000..1461ff6dd
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerPoolConfigMapper.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.customer;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 客户公海配置 Mapper
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmCustomerPoolConfigMapper extends BaseMapperX<CrmCustomerPoolConfigDO> {
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigService.java
new file mode 100644
index 000000000..cba8f87cd
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigService.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.crm.service.customer;
+
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPoolConfigUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
+
+import javax.validation.Valid;
+
+/**
+ * 客户公海配置 Service 接口
+ *
+ * @author Wanwan
+ */
+public interface CrmCustomerPoolConfigService {
+
+    /**
+     * 获得客户公海配置
+     *
+     * @return 客户公海配置
+     */
+    CrmCustomerPoolConfigDO getCustomerPoolConfig();
+
+    /**
+     * 保存客户公海配置
+     *
+     * @param saveReqVO 更新信息
+     */
+    void updateCustomerPoolConfig(@Valid CrmCustomerPoolConfigUpdateReqVO saveReqVO);
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
new file mode 100644
index 000000000..5c3099875
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.crm.service.customer;
+
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPoolConfigUpdateReqVO;
+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 org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Objects;
+
+/**
+ * 客户公海配置 Service 实现类
+ *
+ * @author Wanwan
+ */
+@Service
+@Validated
+public class CrmCustomerPoolConfigServiceImpl implements CrmCustomerPoolConfigService {
+    @Resource
+    private CrmCustomerPoolConfigMapper customerPoolConfigMapper;
+
+    /**
+     * 获得客户公海配置
+     *
+     * @return 客户公海配置
+     */
+    @Override
+    public CrmCustomerPoolConfigDO getCustomerPoolConfig() {
+        return customerPoolConfigMapper.selectOne(new LambdaQueryWrapperX<CrmCustomerPoolConfigDO>().last("LIMIT 1"));
+    }
+
+    /**
+     * 保存客户公海配置
+     *
+     * @param saveReqVO 更新信息
+     */
+    @Override
+    public void updateCustomerPoolConfig(CrmCustomerPoolConfigUpdateReqVO saveReqVO) {
+        // 存在,则进行更新
+        CrmCustomerPoolConfigDO dbConfig = getCustomerPoolConfig();
+        if (Objects.nonNull(dbConfig)) {
+            customerPoolConfigMapper.updateById(CrmCustomerConvert.INSTANCE.convert(saveReqVO).setId(dbConfig.getId()));
+            return;
+        }
+        // 不存在,则进行插入
+        customerPoolConfigMapper.insert(CrmCustomerConvert.INSTANCE.convert(saveReqVO));
+    }
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index e4fafd5cc..86c6cf9af 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -1,7 +1,5 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.collection.ListUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
@@ -12,13 +10,11 @@ import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 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.dept.DeptApi;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerPoolConfigMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerPoolConfigMapper.xml
new file mode 100644
index 000000000..0b7097713
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerPoolConfigMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerPoolConfigMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

From b06432b8f8c1157e6cb68d2eeb9ba656ca762a42 Mon Sep 17 00:00:00 2001
From: xiaqing <xiaqing@bonc>
Date: Wed, 8 Nov 2023 10:46:59 +0800
Subject: [PATCH 074/101] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=A2=E6=88=B7?=
 =?UTF-8?q?=E7=9A=84=E5=85=AC=E6=B5=B7=E9=A2=86=E5=8F=96=E5=92=8C=E5=88=86?=
 =?UTF-8?q?=E9=85=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/crm/enums/ErrorCodeConstants.java  |  4 ++
 .../admin/customer/CrmCustomerController.java | 28 ++++++++++
 .../service/customer/CrmCustomerService.java  | 16 ++++++
 .../customer/CrmCustomerServiceImpl.java      | 56 ++++++++++++++++++-
 4 files changed, 103 insertions(+), 1 deletion(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 3a6e3713b..360c2f082 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -34,6 +34,10 @@ public interface ErrorCodeConstants {
 
     // ========== 客户管理 1_020_006_000 ==========
     ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
+    ErrorCode CUSTOMER_OWNER_EXISTS = new ErrorCode(1_020_006_001, "客户已存在所属负责人");
+    ErrorCode CUSTOMER_LOCKED = new ErrorCode(1_020_006_002, "客户状态已锁定");
+    ErrorCode CUSTOMER_DEALED = new ErrorCode(1_020_006_003, "客户已交易");
+
 
     // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 10b6b5ee8..f6e8fee72 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
@@ -21,6 +23,7 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.CollectionUtils;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -32,6 +35,7 @@ import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -152,4 +156,28 @@ public class CrmCustomerController {
         return success(true);
     }
 
+    @PutMapping("/receive")
+    @Operation(summary = "根据客户id领取公海任务")
+    @PreAuthorize("@ss.hasPermission('crm:customer:receive')")
+    public CommonResult<String>  receiveByIds(List<Long> cIds){
+        // 判断是否为空
+        if(CollectionUtils.isEmpty(cIds))
+            return error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),GlobalErrorCodeConstants.BAD_REQUEST.getMsg());
+        // 领取公海任务
+        customerService.receive(cIds);
+        return success("领取成功");
+    }
+
+    @PutMapping("/distributeByIds")
+    @Operation(summary = "分配公海给对应负责人")
+    @PreAuthorize("@ss.hasPermission('crm:customer:distributeByIds')")
+    public CommonResult<String> distributeByIds(Long ownerId,List<Long>cIds){
+        //判断参数不能为空
+        if(ownerId==null || CollectionUtils.isEmpty(cIds))
+            return error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),GlobalErrorCodeConstants.BAD_REQUEST.getMsg());
+
+        return success("分配成功");
+    }
+
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index 503027e5f..cc7bb2151 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -85,4 +85,20 @@ public interface CrmCustomerService {
      */
     void lockCustomer(@Valid CrmCustomerUpdateReqVO updateReqVO);
 
+    /**
+     * 描述    :接受公海客户
+     * Author :xiaqing
+     * Date   :2023-11-07 22:47:40
+     */
+    void receive(List<Long>ids);
+
+    /**
+     *
+     *功能描述: 分配负责人
+     * @param cIds 要分配的客户id
+     * @param ownerId 分配的负责人id
+     * @author xiaqing
+     * @date 2023-11-08 10:40:22
+     */
+    void distributeByIds(List<Long>cIds,Long ownerId);
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index e4fafd5cc..301fe6f57 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.service.customer;
 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.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
@@ -18,12 +19,13 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 
 /**
  * 客户 Service 实现类
@@ -142,4 +144,56 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         customerMapper.updateById(updateObj);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void receive(List <Long> ids) {
+        transferCustomerOwner(ids,SecurityFrameworkUtils.getLoginUserId());
+    }
+
+    @Override
+    public void distributeByIds(List <Long> cIds, Long ownerId) {
+        transferCustomerOwner(cIds,ownerId);
+    }
+
+    private void transferCustomerOwner(List <Long> cIds, Long ownerId){
+        //先一次性校验完成客户是否可用
+        for (Long cId : cIds) {
+            //校验是否存在
+            validateCustomerExists(cId);
+            //todo 校验是否已有负责人
+            validCustomerOwnerExist(cId);
+            //todo 校验是否锁定
+            validCustomerIsLocked(cId);
+            //todo 校验成交状态
+            validCustomerDeal(cId);
+        }
+        List<CrmCustomerDO> updateDos = new ArrayList <>();
+        for (Long cId : cIds){
+            CrmCustomerDO customerDO = new CrmCustomerDO();
+            customerDO.setId(cId);
+            customerDO.setOwnerUserId(SecurityFrameworkUtils.getLoginUserId());
+        }
+        //统一修改状态
+        customerMapper.updateBatch(updateDos);
+    }
+
+    private void validCustomerOwnerExist(Long id) {
+        if (customerMapper.selectById(id).getOwnerUserId()!=null) {
+            throw exception(CUSTOMER_OWNER_EXISTS);
+        }
+    }
+
+    private void validCustomerIsLocked(Long id) {
+        if (customerMapper.selectById(id).getLockStatus() ==true) {
+            throw exception(CUSTOMER_LOCKED);
+        }
+    }
+
+    private void validCustomerDeal(Long id) {
+        if (customerMapper.selectById(id).getDealStatus() ==true) {
+            throw exception(CUSTOMER_DEALED);
+        }
+    }
+
+
 }

From 18de22dc390e480c625b1ebe89ee807ed146ae43 Mon Sep 17 00:00:00 2001
From: xiaqing <xiaqing@bonc>
Date: Wed, 8 Nov 2023 10:51:10 +0800
Subject: [PATCH 075/101] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=A2=E6=88=B7?=
 =?UTF-8?q?=E7=9A=84=E5=85=AC=E6=B5=B7=E9=A2=86=E5=8F=96=E5=92=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../crm/controller/admin/customer/CrmCustomerController.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index f6e8fee72..79589e04d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -175,7 +175,7 @@ public class CrmCustomerController {
         //判断参数不能为空
         if(ownerId==null || CollectionUtils.isEmpty(cIds))
             return error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),GlobalErrorCodeConstants.BAD_REQUEST.getMsg());
-
+        customerService.distributeByIds(cIds,ownerId);
         return success("分配成功");
     }
 

From daa7d14b092342b1bd7062873c9c9b7ec79cc2bc Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 14:36:44 +0800
Subject: [PATCH 076/101] =?UTF-8?q?reafactor:=20=E4=BC=9A=E5=91=98?=
 =?UTF-8?q?=E5=95=86=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mybatis/core/mapper/BaseMapperX.java      |  4 +-
 .../favorite/ProductFavoriteController.java   | 41 +++++--------------
 .../favorite/ProductFavoriteConvert.java      | 11 ++---
 .../favorite/ProductFavoriteDetailDO.java     |  1 -
 .../mysql/favorite/ProductFavoriteMapper.java | 33 ++-------------
 .../favorite/ProductFavoriteService.java      |  3 +-
 .../favorite/ProductFavoriteServiceImpl.java  |  5 +--
 7 files changed, 24 insertions(+), 74 deletions(-)

diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index 2654bec52..db054e972 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -133,9 +133,9 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         Db.saveOrUpdateBatch(collection);
     }
 
-    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, @Param("resultTypeClass_Eg1sG") Class<DTO> var2, @Param("ew") MPJBaseJoin<T> queryWrapper) {
+    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
         IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
-        selectJoinPage(mpPage, var2, queryWrapper);
+        selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
         // 转换返回
         return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
     }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
index e59c10827..9157e8a05 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
@@ -4,16 +4,15 @@ import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
-import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteBatchReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import cn.iocoder.yudao.module.product.service.favorite.ProductFavoriteService;
+import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
 import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
@@ -24,6 +23,7 @@ import javax.validation.Valid;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 
 @Tag(name = "管理后台 - 商品收藏")
 @RestController
@@ -34,6 +34,9 @@ public class ProductFavoriteController {
     @Resource
     private ProductFavoriteService productFavoriteService;
 
+    @Resource
+    private ProductSpuService productSpuService;
+
     @PostMapping("/create")
     @Operation(summary = "添加单个商品收藏")
     @PreAuthorize("@ss.hasPermission('product:favorite:create')")
@@ -41,14 +44,6 @@ public class ProductFavoriteController {
         return success(productFavoriteService.createFavorite(reqVO.getUserId(), reqVO.getSpuId()));
     }
 
-    @PostMapping("/create-list")
-    @Operation(summary = "添加多个商品收藏")
-    @PreAuthorize("@ss.hasPermission('product:favorite:create')")
-    public CommonResult<Boolean> createFavoriteList(@Valid @RequestBody ProductFavoriteBatchReqVO reqVO) {
-        // todo @jason:待实现;如果有已经收藏的,不用报错,忽略即可;
-        return success(Boolean.TRUE);
-    }
-
     @DeleteMapping("/delete")
     @Operation(summary = "取消单个商品收藏")
     @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
@@ -57,26 +52,19 @@ public class ProductFavoriteController {
         return success(Boolean.TRUE);
     }
 
-    @DeleteMapping("/delete-list")
-    @Operation(summary = "取消单个商品收藏")
-    @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
-    public CommonResult<Boolean> deleteFavoriteList(@Valid @RequestBody ProductFavoriteBatchReqVO reqVO) {
-        // todo @jason:待实现
-//        productFavoriteService.deleteFavorite(getLoginUserId(), reqVO.getSpuId());
-        return success(Boolean.TRUE);
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得商品收藏分页")
     @PreAuthorize("@ss.hasPermission('product:favorite:query')")
     public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
-        PageResult<ProductFavoriteDetailDO> favoritePage = productFavoriteService.getFavoritePageByFilter(pageVO);
+        PageResult<ProductFavoriteDO> favoritePage = productFavoriteService.getFavoritePage(pageVO);
         if (CollUtil.isEmpty(favoritePage.getList())) {
             return success(PageResult.empty());
         }
 
+        List<ProductSpuDO> list = productSpuService.getSpuList(convertSet(favoritePage.getList(), ProductFavoriteDO::getSpuId));
+
         // 得到商品 spu 信息
-        List<ProductFavoriteRespVO> favorites =  ProductFavoriteConvert.INSTANCE.convertList2admin(favoritePage.getList());
+        List<ProductFavoriteRespVO> favorites =  ProductFavoriteConvert.INSTANCE.convertList2admin(favoritePage.getList(), list);
 
         // 转换 VO 结果
         PageResult<ProductFavoriteRespVO> pageResult = new PageResult<>(favoritePage.getTotal());
@@ -92,13 +80,4 @@ public class ProductFavoriteController {
         ProductFavoriteDO favorite = productFavoriteService.getFavorite(reqVO.getUserId(), reqVO.getSpuId());
         return success(favorite != null);
     }
-
-    @GetMapping(value = "/get-count")
-    @Operation(summary = "获得商品收藏数量")
-    @Parameter(name = "userId", description = "用户编号", required = true)
-    @PreAuthenticated
-    public CommonResult<Long> getFavoriteCount(@RequestParam("userId") Long userId) {
-        return success(productFavoriteService.getFavoriteCount(userId));
-    }
-
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
index 22ffb8577..9adac8c86 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
@@ -13,6 +13,7 @@ import org.mapstruct.factory.Mappers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
@@ -41,13 +42,13 @@ public interface ProductFavoriteConvert {
     @Mapping(target = "userId", source = "favorite.userId")
     @Mapping(target = "spuId", source = "favorite.spuId")
     @Mapping(target = "createTime", source = "favorite.createTime")
-    @Mapping(target = "favoriteStatus", constant = "1")
     ProductFavoriteRespVO convert2admin(ProductSpuDO spu, ProductFavoriteDO favorite);
 
-    default List<ProductFavoriteRespVO> convertList2admin(List<ProductFavoriteDetailDO> favorites) {
-        List<ProductFavoriteRespVO> resultList = new ArrayList<>(favorites.size());
-        for (ProductFavoriteDetailDO favorite : favorites) {
-            resultList.add(convert2admin(favorite.getSpuDO(), favorite));
+    default List<ProductFavoriteRespVO> convertList2admin(List<ProductFavoriteDO> favorites, List<ProductSpuDO> spus) {
+        List<ProductFavoriteRespVO> resultList = new ArrayList<>(spus.size());
+        for (ProductFavoriteDO favorite : favorites) {
+            Optional<ProductSpuDO> spu =  spus.stream().filter(e -> e.getId().equals(favorite.getSpuId())).findFirst();
+            resultList.add(convert2admin(spu.get(), favorite));
         }
         return resultList;
     }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
index ba4d5b557..60e401e11 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
@@ -12,5 +12,4 @@ import lombok.Data;
 public class ProductFavoriteDetailDO extends ProductFavoriteDO {
 
     ProductSpuDO spuDO;
-
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
index 08a5c3063..e116a7c4a 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
@@ -5,12 +5,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
@@ -27,34 +23,11 @@ public interface ProductFavoriteMapper extends BaseMapperX<ProductFavoriteDO> {
                 .orderByDesc(ProductFavoriteDO::getId));
     }
 
-    default PageResult<ProductFavoriteDetailDO> selectPageByUserAndFields(ProductFavoritePageReqVO reqVO) {
-        MPJLambdaWrapper<ProductFavoriteDO> wrapper =  new MPJLambdaWrapper<ProductFavoriteDO>()
+    default PageResult<ProductFavoriteDO> selectPageByUserId(ProductFavoritePageReqVO reqVO) {
+        return selectPage(reqVO, new MPJLambdaWrapper<ProductFavoriteDO>()
                 .selectAll(ProductFavoriteDO.class)
                 .eq(ProductFavoriteDO::getUserId, reqVO.getUserId())
-                .selectAssociation(ProductSpuDO.class, ProductFavoriteDetailDO::getSpuDO);
-        if(StringUtils.isNotEmpty(reqVO.getName())){
-            wrapper.likeRight(ProductSpuDO::getName, reqVO.getName());
-        }
-        if(StringUtils.isNotEmpty(reqVO.getName()) && StringUtils.isNotEmpty(reqVO.getKeyword())){
-            wrapper.or();
-        }
-        if(StringUtils.isNotEmpty(reqVO.getKeyword())){
-            wrapper.likeRight(ProductSpuDO::getKeyword, reqVO.getKeyword());
-        }
-
-        if(reqVO.getCreateTime() != null){
-            if (reqVO.getCreateTime()[0] != null && reqVO.getCreateTime()[1] != null) {
-                wrapper.between(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[0], reqVO.getCreateTime()[1]);
-            }
-            if (reqVO.getCreateTime()[0] != null) {
-                wrapper.ge(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[0]);
-            }
-            if (reqVO.getCreateTime()[1] != null) {
-                wrapper.le(ProductFavoriteDO::getCreateTime, reqVO.getCreateTime()[1]);
-            }
-        }
-
-        return selectJoinPage(reqVO, ProductFavoriteDetailDO.class, wrapper);
+                .orderByDesc(ProductFavoriteDO::getCreateTime));
     }
 
     default Long selectCountByUserId(Long userId) {
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
index 87c9854a9..295af4c94 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
@@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 
 import javax.validation.Valid;
 
@@ -44,7 +43,7 @@ public interface ProductFavoriteService {
      *
      * @param reqVO 请求 vo
      */
-    PageResult<ProductFavoriteDetailDO> getFavoritePageByFilter(@Valid ProductFavoritePageReqVO reqVO);
+    PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO);
 
     /**
      * 获取收藏过商品
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
index 0f8d30ec0..945201286 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
@@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavor
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 import cn.iocoder.yudao.module.product.dal.mysql.favorite.ProductFavoriteMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -56,8 +55,8 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
     }
 
     @Override
-    public PageResult<ProductFavoriteDetailDO> getFavoritePageByFilter(@Valid  ProductFavoritePageReqVO reqVO) {
-        return productFavoriteMapper.selectPageByUserAndFields(reqVO);
+    public PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO) {
+        return productFavoriteMapper.selectPageByUserId(reqVO);
     }
 
     @Override

From 5facac733039e924c293ae892d289bd154187e9d Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 14:37:18 +0800
Subject: [PATCH 077/101] =?UTF-8?q?refactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../favorite/vo/ProductFavoriteBaseVO.java    |  2 --
 .../vo/ProductFavoriteBatchReqVO.java         | 21 -------------------
 .../favorite/vo/ProductFavoritePageReqVO.java | 18 ----------------
 .../favorite/vo/ProductFavoriteRespVO.java    |  4 ----
 4 files changed, 45 deletions(-)
 delete mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
index 72b2613d5..68b0a0a16 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBaseVO.java
@@ -16,6 +16,4 @@ public class ProductFavoriteBaseVO {
     @NotNull(message = "用户编号不能为空")
     private Long userId;
 
-
-
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
deleted file mode 100644
index d779ff3a8..000000000
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteBatchReqVO.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package cn.iocoder.yudao.module.product.controller.admin.favorite.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.ToString;
-
-import javax.validation.constraints.NotNull;
-import java.util.List;
-
-import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
-
-@Schema(description = "管理后台 - 商品收藏的批量 Request VO")
-@Data
-@ToString(callSuper = true)
-public class ProductFavoriteBatchReqVO extends ProductFavoriteBaseVO{
-
-    @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502")
-    @NotNull(message = "商品 SPU 编号数组不能为空")
-    private List<Long> spuIds;
-
-}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
index 37f8cecc3..9c0b33035 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
@@ -5,11 +5,6 @@ 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
@@ -19,17 +14,4 @@ public class ProductFavoritePageReqVO extends PageParam {
 
     @Schema(description = "用户编号", example = "5036")
     private Long userId;
-
-    @Schema(description = "商品 SPU 编号", example = "32734")
-    private Long spuId;
-
-    @Schema(description = "收藏时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] createTime;
-
-    @Schema(description = "商品名称", example = "5036")
-    private String name;
-
-    @Schema(description = "关键字", example = "5036")
-    private String keyword;
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
index 255fc631b..bbf972181 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
@@ -15,8 +15,4 @@ public class ProductFavoriteRespVO  extends ProductSpuRespVO {
 
     @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
     private Long spuId;
-
-    @Schema(description = "收藏状态", example = "1")
-    private Integer favoriteStatus;
-
 }

From c8ed99a5a41a1efdf8f296f431f9331e718f4f6d Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Wed, 8 Nov 2023 14:41:51 +0800
Subject: [PATCH 078/101] =?UTF-8?q?CRM-=E5=AE=A2=E6=88=B7=EF=BC=9A?=
 =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=AE=A2=E6=88=B7=E8=AF=A6=E6=83=85=E3=80=81?=
 =?UTF-8?q?=E5=88=86=E9=A1=B5=E3=80=81=E5=85=AC=E6=B5=B7=E6=95=B0=E6=8D=AE?=
 =?UTF-8?q?=E8=8E=B7=E5=8F=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/customer/CrmCustomerController.java | 98 +++++++++++--------
 .../admin/customer/vo/CrmCustomerRespVO.java  |  2 +-
 .../convert/business/CrmBusinessConvert.java  |  3 -
 .../convert/customer/CrmCustomerConvert.java  | 49 +++++++++-
 .../dataobject/customer/CrmCustomerDO.java    | 22 +----
 .../dal/mysql/business/CrmBusinessMapper.java |  7 +-
 .../dal/mysql/customer/CrmCustomerMapper.java | 26 ++---
 .../mysql/permission/CrmPermissionMapper.java | 18 ++--
 .../business/CrmBusinessServiceImpl.java      | 11 +--
 .../service/customer/CrmCustomerService.java  |  4 +-
 .../customer/CrmCustomerServiceImpl.java      | 36 +++++--
 .../permission/CrmPermissionService.java      | 22 +++--
 .../permission/CrmPermissionServiceImpl.java  | 12 ++-
 .../permission/bo/CrmPermissionPageReqBO.java | 32 ------
 .../customer/CrmCustomerServiceImplTest.java  |  2 +-
 15 files changed, 192 insertions(+), 152 deletions(-)
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionPageReqBO.java

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 10b6b5ee8..0354afbb5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -1,22 +1,21 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.NumberUtil;
-import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
-import com.google.common.collect.Lists;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -28,11 +27,14 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.IOException;
-import java.util.*;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -48,6 +50,8 @@ public class CrmCustomerController {
     private DeptApi deptApi;
     @Resource
     private AdminUserApi adminUserApi;
+    @Resource
+    private CrmPermissionService permissionService;
 
     @PostMapping("/create")
     @Operation(summary = "创建客户")
@@ -78,49 +82,57 @@ public class CrmCustomerController {
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<CrmCustomerRespVO> getCustomer(@RequestParam("id") Long id) {
+        // 1. 获取客户
         CrmCustomerDO customer = customerService.getCustomer(id);
-        CrmCustomerRespVO customerRespVO = CrmCustomerConvert.INSTANCE.convert(customer);
-        if (ObjectUtil.isAllNotEmpty(customer, customer.getAreaId())) {
-            customerRespVO.setAreaName(AreaUtils.format(customer.getAreaId()));
+        if (customer == null) {
+            return success(null);
         }
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(CollUtil.removeNull(Lists.newArrayList(NumberUtil.parseLong(customerRespVO.getCreator()), customerRespVO.getOwnerUserId())));
-        customerRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(customerRespVO.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
-        AdminUserRespDTO ownerUser = userMap.get(customer.getOwnerUserId());
-        if (Objects.nonNull(ownerUser)) {
-            customerRespVO.setOwnerUserName(ownerUser.getNickname());
-            DeptRespDTO dept = deptApi.getDept(ownerUser.getDeptId());
-            if (Objects.nonNull(dept)) {
-                customerRespVO.setOwnerUserDept(dept.getName());
-            }
-        }
-        return success(customerRespVO);
+
+        // 2. 拼接数据
+        // 2.1 获取负责人
+        List<CrmPermissionDO> ownerList = permissionService.getPermissionByBizTypeAndBizIdsAndLevel(
+                CrmBizTypeEnum.CRM_CUSTOMER.getType(), Collections.singletonList(customer.getId()),
+                CrmPermissionLevelEnum.OWNER.getLevel());
+        Map<Long, CrmPermissionDO> ownerMap = convertMap(ownerList, CrmPermissionDO::getBizId);
+        // 2.2 获取负责人详情
+        Set<Long> userIds = convertSet(ownerList, CrmPermissionDO::getUserId);
+        userIds.add(Long.parseLong(customer.getCreator())); // 加入创建者
+        List<AdminUserRespDTO> userList = adminUserApi.getUserList(userIds);
+        Map<Long, AdminUserRespDTO> userMap = convertMap(userList, AdminUserRespDTO::getId);
+        // 2.3 获取部门详情
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userList, AdminUserRespDTO::getDeptId));
+        return success(CrmCustomerConvert.INSTANCE.convert(customer, ownerMap, userMap, deptMap));
     }
 
     @GetMapping("/page")
     @Operation(summary = "获得客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
-        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO);
-        PageResult<CrmCustomerRespVO> pageVo = CrmCustomerConvert.INSTANCE.convertPage(pageResult);
-        // TODO @wanwan: 可以参考 CollectionUtils.convertListByFlatMap(),目的是简洁
-        Set<Long> userSet = pageVo.getList().stream().flatMap(i -> Stream.of(NumberUtil.parseLong(i.getCreator()), i.getOwnerUserId())).collect(Collectors.toSet());
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userSet);
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(userMap.values().stream().map(AdminUserRespDTO::getDeptId).collect(Collectors.toSet()));
-        // TODO @wanwan:这块可以形成一个 convertPage 方法,default 实现;
-        pageVo.getList().forEach(customerRespVO -> {
-            customerRespVO.setAreaName(AreaUtils.format(customerRespVO.getAreaId()));
-            customerRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(customerRespVO.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
-            // TODO @wanwan:可以使用 MapUtils.findAndThen
-            AdminUserRespDTO ownerUser = userMap.get(customerRespVO.getOwnerUserId());
-            if (Objects.nonNull(ownerUser)) {
-                customerRespVO.setOwnerUserName(ownerUser.getNickname());
-                DeptRespDTO dept = deptMap.get(ownerUser.getDeptId());
-                if (Objects.nonNull(dept)) {
-                    customerRespVO.setOwnerUserDept(dept.getName());
-                }
-            }
-        });
-        return success(pageVo);
+        return convertPage(customerService.getCustomerPage(pageVO, getLoginUserId()));
+    }
+
+    @GetMapping("/pool-page")
+    @Operation(summary = "获得公海客户分页")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<PageResult<CrmCustomerRespVO>> getPoolCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
+        return convertPage(customerService.getCustomerPage(pageVO, CrmPermissionDO.POOL_USER_ID));
+    }
+
+    private CommonResult<PageResult<CrmCustomerRespVO>> convertPage(PageResult<CrmCustomerDO> pageResult) {
+        // 2. 拼接数据
+        Set<Long> ids = convertSet(pageResult.getList(), CrmCustomerDO::getId);
+        // 2.1 获取负责人
+        List<CrmPermissionDO> ownerList = permissionService.getPermissionByBizTypeAndBizIdsAndLevel(
+                CrmBizTypeEnum.CRM_CUSTOMER.getType(), ids, CrmPermissionLevelEnum.OWNER.getLevel());
+        Map<Long, CrmPermissionDO> ownerMap = convertMap(ownerList, CrmPermissionDO::getBizId);
+        // 2.2 获取负责人详情
+        Set<Long> userIds = convertSet(ownerList, CrmPermissionDO::getUserId);
+        userIds.addAll(convertSet(pageResult.getList(), item -> Long.parseLong(item.getCreator()))); // 加入创建者
+        List<AdminUserRespDTO> userList = adminUserApi.getUserList(userIds);
+        Map<Long, AdminUserRespDTO> userMap = convertMap(userList, AdminUserRespDTO::getId);
+        // 2.3 获取部门详情
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userList, AdminUserRespDTO::getDeptId));
+        return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, ownerMap, userMap, deptMap));
     }
 
     @GetMapping("/export-excel")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
index f37740f26..2cbd85dd3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java
@@ -33,7 +33,7 @@ public class CrmCustomerRespVO extends CrmCustomerBaseVO {
     @Schema(description = "负责人名字", example = "25682")
     private String ownerUserName;
     @Schema(description = "负责人部门")
-    private String ownerUserDept;
+    private String ownerUserDeptName;
 
     @Schema(description = "地区名称", example = "北京市")
     private String areaName;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
index 4256a6463..8b7f7e83b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.crm.convert.business;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
@@ -38,6 +37,4 @@ public interface CrmBusinessConvert {
     })
     CrmPermissionTransferReqBO convert(CrmBusinessTransferReqVO reqVO, Long userId);
 
-    CrmPermissionPageReqBO convert(CrmBusinessPageReqVO pageReqVO);
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index f8b47ef19..dbcc0a606 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -1,15 +1,22 @@
 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.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 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;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 
 /**
  * 客户 Convert
@@ -27,7 +34,24 @@ public interface CrmCustomerConvert {
 
     CrmCustomerRespVO convert(CrmCustomerDO bean);
 
-    PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> page);
+    default CrmCustomerRespVO convert(CrmCustomerDO customer, Map<Long, CrmPermissionDO> ownerMap,
+                                      Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
+        CrmCustomerRespVO customerResp = convert(customer);
+        findAndThen(ownerMap, customerResp.getId(), owner -> {
+            customerResp.setOwnerUserId(owner.getUserId());
+            customerResp.setAreaName(AreaUtils.format(customerResp.getAreaId()));
+            findAndThen(userMap, owner.getUserId(), user -> {
+                customerResp.setOwnerUserName(user.getNickname());
+            });
+            findAndThen(userMap, Long.parseLong(customerResp.getCreator()), user -> {
+                customerResp.setCreatorName(user.getNickname());
+            });
+            findAndThen(deptMap, customerResp.getOwnerUserId(), dept -> {
+                customerResp.setOwnerUserDeptName(dept.getName());
+            });
+        });
+        return customerResp;
+    }
 
     List<CrmCustomerExcelVO> convertList02(List<CrmCustomerDO> list);
 
@@ -37,4 +61,27 @@ public interface CrmCustomerConvert {
     })
     CrmPermissionTransferReqBO convert(CrmCustomerTransferReqVO reqVO, Long userId);
 
+    PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> page);
+
+    default PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> pageResult, Map<Long, CrmPermissionDO> ownerMap,
+                                                      Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
+        PageResult<CrmCustomerRespVO> result = convertPage(pageResult);
+        result.getList().forEach(item -> {
+            findAndThen(ownerMap, item.getId(), owner -> {
+                item.setOwnerUserId(owner.getUserId());
+                item.setAreaName(AreaUtils.format(item.getAreaId()));
+                findAndThen(userMap, owner.getUserId(), user -> {
+                    item.setOwnerUserName(user.getNickname());
+                });
+                findAndThen(userMap, Long.parseLong(item.getCreator()), user -> {
+                    item.setCreatorName(user.getNickname());
+                });
+                findAndThen(deptMap, item.getOwnerUserId(), dept -> {
+                    item.setOwnerUserDeptName(dept.getName());
+                });
+            });
+        });
+        return result;
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index 3e00b1cc5..f463d309e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.customer;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -49,19 +50,19 @@ public class CrmCustomerDO extends BaseDO {
     /**
      * 所属行业
      *
-     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_CUSTOMER_INDUSTRY}
+     * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_INDUSTRY}
      */
     private Integer industryId;
     /**
      * 客户等级
      *
-     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_CUSTOMER_LEVEL}
+     * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_LEVEL}
      */
     private Integer level;
     /**
      * 客户来源
      *
-     * 对应字典 {@link cn.iocoder.yudao.module.crm.enums.DictTypeConstants#CRM_CUSTOMER_SOURCE}
+     * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_SOURCE}
      */
     private Integer source;
     /**
@@ -96,21 +97,6 @@ public class CrmCustomerDO extends BaseDO {
      * 备注
      */
     private String remark;
-    /**
-     * 负责人的用户编号
-     *
-     * 关联 AdminUserDO 的 id 字段
-     */
-    private Long ownerUserId;
-    // TODO @puhui999:这块抽到 permission 里;
-    /**
-     * 只读权限的用户编号数组
-     */
-    private String roUserIds;
-    /**
-     * 读写权限的用户编号数组
-     */
-    private String rwUserIds;
     /**
      * 地区编号
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
index 0ae83c236..f8f856104 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.business;
 
+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.business.vo.CrmBusinessExportReqVO;
@@ -18,14 +19,14 @@ import java.util.List;
 @Mapper
 public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
 
-    default List<CrmBusinessDO> selectList(CrmBusinessPageReqVO reqVO, Collection<Long> ids) {
-        return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
+    default PageResult<CrmBusinessDO> selectPage(CrmBusinessPageReqVO reqVO, Collection<Long> ids) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmBusinessDO>()
                 .in(CrmBusinessDO::getId, ids)
                 .likeIfPresent(CrmBusinessDO::getName, reqVO.getName())
                 .orderByDesc(CrmBusinessDO::getId));
     }
 
-    default List<CrmBusinessDO> selectList(CrmBusinessExportReqVO reqVO) {
+    default List<CrmBusinessDO> selectPage(CrmBusinessExportReqVO reqVO) {
         return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
                 .likeIfPresent(CrmBusinessDO::getName, reqVO.getName())
                 .eqIfPresent(CrmBusinessDO::getStatusTypeId, reqVO.getStatusTypeId())
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
index 74cdfe5dd..683df3581 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -3,12 +3,11 @@ package cn.iocoder.yudao.module.crm.dal.mysql.customer;
 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.customer.vo.CrmCustomerExportReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import org.apache.ibatis.annotations.Mapper;
 
-import java.util.List;
+import java.util.Collection;
 
 /**
  * 客户 Mapper
@@ -18,21 +17,14 @@ import java.util.List;
 @Mapper
 public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
 
-    default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<CrmCustomerDO>()
-                .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
-                .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
-                .eqIfPresent(CrmCustomerDO::getIndustryId, reqVO.getIndustryId())
-                .eqIfPresent(CrmCustomerDO::getLevel, reqVO.getLevel())
-                .eqIfPresent(CrmCustomerDO::getSource, reqVO.getSource())
-                .orderByDesc(CrmCustomerDO::getId));
-    }
-
-    default List<CrmCustomerDO> selectList(CrmCustomerExportReqVO reqVO) {
-        return selectList(new LambdaQueryWrapperX<CrmCustomerDO>()
-                .likeIfPresent(CrmCustomerDO::getName, reqVO.getName())
-                .eqIfPresent(CrmCustomerDO::getMobile, reqVO.getMobile())
-                .orderByDesc(CrmCustomerDO::getId));
+    default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO pageReqVO, Collection<Long> ids) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<CrmCustomerDO>()
+                .inIfPresent(CrmCustomerDO::getId, ids)
+                .likeIfPresent(CrmCustomerDO::getName, pageReqVO.getName())
+                .eqIfPresent(CrmCustomerDO::getMobile, pageReqVO.getMobile())
+                .eqIfPresent(CrmCustomerDO::getIndustryId, pageReqVO.getIndustryId())
+                .eqIfPresent(CrmCustomerDO::getLevel, pageReqVO.getLevel())
+                .eqIfPresent(CrmCustomerDO::getSource, pageReqVO.getSource()));
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index 3b9a220fc..34a80c2c4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -1,12 +1,11 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.permission;
 
-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.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -30,10 +29,17 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 .eq(CrmPermissionDO::getBizId, bizId));
     }
 
-    default PageResult<CrmPermissionDO> selectPage(CrmPermissionPageReqBO pageReqBO) {
-        return selectPage(pageReqBO, new LambdaQueryWrapperX<CrmPermissionDO>()
-                .eq(CrmPermissionDO::getBizType, pageReqBO.getBizType())
-                .eq(CrmPermissionDO::getUserId, pageReqBO.getUserId())); // 只要是团队成员都有读取的权限
+    default List<CrmPermissionDO> selectListByBizTypeAndUserId(Integer bizType, Long userId) {
+        return selectList(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getBizType, bizType)
+                .eq(CrmPermissionDO::getUserId, userId));
+    }
+
+    default List<CrmPermissionDO> selectListByBizTypeAndBizIdsAndLevel(Integer bizType, Collection<Long> bizIds, Integer level) {
+        return selectList(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getBizType, bizType)
+                .in(CrmPermissionDO::getBizId, bizIds)
+                .eq(CrmPermissionDO::getLevel, level));
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index d19d8de46..bb154d056 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -103,21 +103,20 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     @Override
     public PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO, Long userId) {
         // 1. 获取当前用户能看的分页数据
-        PageResult<CrmPermissionDO> permissionPage = crmPermissionService.getPermissionPage(
-                CrmBusinessConvert.INSTANCE.convert(pageReqVO).setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()).setUserId(userId));
-        Set<Long> ids = convertSet(permissionPage.getList(), CrmPermissionDO::getBizId);
+        List<CrmPermissionDO> permissions = crmPermissionService.getPermissionListByBizTypeAndUserId(
+                CrmBizTypeEnum.CRM_BUSINESS.getType(), userId);
+        Set<Long> ids = convertSet(permissions, CrmPermissionDO::getBizId);
         if (CollUtil.isEmpty(ids)) { // 没得说明没有什么给他看的
             return PageResult.empty();
         }
 
         // 2. 获取商机分页数据
-        List<CrmBusinessDO> businessList = businessMapper.selectList(pageReqVO, ids);
-        return new PageResult<>(businessList, (long) businessList.size());
+        return businessMapper.selectPage(pageReqVO, ids);
     }
 
     @Override
     public List<CrmBusinessDO> getBusinessList(CrmBusinessExportReqVO exportReqVO) {
-        return businessMapper.selectList(exportReqVO);
+        return businessMapper.selectPage(exportReqVO);
     }
 
     @Override
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index 503027e5f..057de7bf9 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 
 import javax.validation.Valid;
-import java.util.Collection;
 import java.util.List;
 
 /**
@@ -50,9 +49,10 @@ public interface CrmCustomerService {
      * 获得客户分页
      *
      * @param pageReqVO 分页查询
+     * @param userId    用户编号
      * @return 客户分页
      */
-    PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO);
+    PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId);
 
     /**
      * 获得客户列表, 用于 Excel 导出
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 2580bc854..56e958334 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -1,9 +1,12 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
@@ -15,10 +18,13 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+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;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
 
 /**
@@ -51,11 +57,10 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, level = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
     public void updateCustomer(CrmCustomerUpdateReqVO updateReqVO) {
         // 校验存在
         validateCustomerExists(updateReqVO.getId());
-        // TODO 芋艿:数据权限,校验是否可以操作
 
         // 更新
         CrmCustomerDO updateObj = CrmCustomerConvert.INSTANCE.convert(updateReqVO);
@@ -64,11 +69,10 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, level = CrmPermissionLevelEnum.WRITE)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteCustomer(Long id) {
         // 校验存在
         validateCustomerExists(id);
-        // TODO 芋艿:数据权限,校验是否可以操作
 
         // 删除
         customerMapper.deleteById(id);
@@ -81,20 +85,34 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     @Override
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, level = CrmPermissionLevelEnum.READ)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#id", level = CrmPermissionLevelEnum.READ)
     public CrmCustomerDO getCustomer(Long id) {
         return customerMapper.selectById(id);
     }
 
     @Override
-    public PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO) {
-        // TODO 芋艿:数据权限,是否可以查询到;
-        return customerMapper.selectPage(pageReqVO);
+    public PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId) {
+        // 1.1 TODO 如果是超级管理员
+        boolean admin = false;
+        if (admin && ObjUtil.notEqual(userId, CrmPermissionDO.POOL_USER_ID)) {
+            return customerMapper.selectPage(pageReqVO, Collections.emptyList());
+        }
+        // 1.2 获取当前用户能看的分页数据
+        List<CrmPermissionDO> permissions = crmPermissionService.getPermissionListByBizTypeAndUserId(
+                CrmBizTypeEnum.CRM_CUSTOMER.getType(), userId);
+        Set<Long> ids = convertSet(permissions, CrmPermissionDO::getBizId);
+        if (CollUtil.isEmpty(ids)) { // 没得说明没有什么给他看的
+            return PageResult.empty();
+        }
+
+        // 2. 获取客户分页数据
+        return customerMapper.selectPage(pageReqVO, ids);
     }
 
     @Override
     public List<CrmCustomerDO> getCustomerList(CrmCustomerExportReqVO exportReqVO) {
-        return customerMapper.selectList(exportReqVO);
+        //return customerMapper.selectList(exportReqVO);
+        return Collections.emptyList();
     }
 
     /**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index 18c7beffc..9bb424305 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -1,15 +1,14 @@
 package cn.iocoder.yudao.module.crm.service.permission;
 
 
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 
 import javax.validation.Valid;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -60,6 +59,16 @@ public interface CrmPermissionService {
      */
     List<CrmPermissionDO> getPermissionByBizTypeAndBizId(Integer bizType, Long bizId);
 
+    /**
+     * 获取数据权限列表,通过 数据类型 x 某个数据
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizIds  数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @param level   权限级别
+     * @return Crm 数据权限列表
+     */
+    List<CrmPermissionDO> getPermissionByBizTypeAndBizIdsAndLevel(Integer bizType, Collection<Long> bizIds, Integer level);
+
     /**
      * 数据权限转移
      *
@@ -68,11 +77,12 @@ public interface CrmPermissionService {
     void transferPermission(@Valid CrmPermissionTransferReqBO crmPermissionTransferReqBO);
 
     /**
-     * 获取数据权限分页数据
+     * 获取用户参与的模块数据列表
      *
-     * @param pageReqBO 分页请求
-     * @return 数据权限分页数据
+     * @param bizType 模块类型
+     * @param userId  用户编号
+     * @return 模块数据列表
      */
-    PageResult<CrmPermissionDO> getPermissionPage(CrmPermissionPageReqBO pageReqBO);
+    List<CrmPermissionDO> getPermissionListByBizTypeAndUserId(Integer bizType, Long userId);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index 4265545a4..b24ae4156 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -2,14 +2,12 @@ package cn.iocoder.yudao.module.crm.service.permission;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionPageReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -18,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
@@ -85,6 +84,11 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         return crmPermissionMapper.selectByBizTypeAndBizId(bizType, bizId);
     }
 
+    @Override
+    public List<CrmPermissionDO> getPermissionByBizTypeAndBizIdsAndLevel(Integer bizType, Collection<Long> bizIds, Integer level) {
+        return crmPermissionMapper.selectListByBizTypeAndBizIdsAndLevel(bizType, bizIds, level);
+    }
+
     private void validateCrmPermissionExists(Long id) {
         if (crmPermissionMapper.selectById(id) == null) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
@@ -133,8 +137,8 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     }
 
     @Override
-    public PageResult<CrmPermissionDO> getPermissionPage(CrmPermissionPageReqBO pageReqBO) {
-        return crmPermissionMapper.selectPage(pageReqBO);
+    public List<CrmPermissionDO> getPermissionListByBizTypeAndUserId(Integer bizType, Long userId) {
+        return crmPermissionMapper.selectListByBizTypeAndUserId(bizType, userId);
     }
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionPageReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionPageReqBO.java
deleted file mode 100644
index 736171c1e..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionPageReqBO.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package cn.iocoder.yudao.module.crm.service.permission.bo;
-
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import cn.iocoder.yudao.framework.common.validation.InEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-import javax.validation.constraints.NotNull;
-
-@Schema(description = "管理后台 - CRM 数据权限分页 Request BO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmPermissionPageReqBO extends PageParam {
-
-    /**
-     * 当前登录用户编号
-     */
-    @NotNull(message = "用户编号不能为空")
-    private Long userId;
-
-    /**
-     * Crm 类型
-     */
-    @NotNull(message = "Crm 类型不能为空")
-    @InEnum(CrmBizTypeEnum.class)
-    private Integer bizType;
-
-}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
index 41dd14f92..4908f79e2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImplTest.java
@@ -130,7 +130,7 @@ public class CrmCustomerServiceImplTest extends BaseDbUnitTest {
         //reqVO.setWebsite(null);
 
         // 调用
-        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(reqVO);
+        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(reqVO, 1L);
         // 断言
         assertEquals(1, pageResult.getTotal());
         assertEquals(1, pageResult.getList().size());

From 388e07c8346ba7d13b963d5885c7a8dc8e58a60d Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Wed, 8 Nov 2023 16:05:23 +0800
Subject: [PATCH 079/101] =?UTF-8?q?CRM=EF=BC=9A=E5=AE=8C=E5=96=84=E6=A8=A1?=
 =?UTF-8?q?=E5=9D=97=E6=95=B0=E6=8D=AE=E6=94=BE=E5=85=A5=E5=85=AC=E6=B5=B7?=
 =?UTF-8?q?=E3=80=81=E5=85=AC=E6=B5=B7=E6=95=B0=E6=8D=AE=E9=A2=86=E5=8F=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../permission/CrmPermissionController.java   | 37 +++++++++++++++----
 .../permission/CrmPermissionService.java      | 18 +++++++++
 .../permission/CrmPermissionServiceImpl.java  | 20 ++++++++++
 3 files changed, 68 insertions(+), 7 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
index 594e132bf..ab9d7907a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
@@ -32,10 +32,13 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - CRM 数据权限(数据团队成员操作)")
 @RestController
@@ -44,7 +47,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
 public class CrmPermissionController {
 
     @Resource
-    private CrmPermissionService crmPermissionService;
+    private CrmPermissionService permissionService;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -59,11 +62,27 @@ public class CrmPermissionController {
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#reqVO.bizType", bizId = "#reqVO.bizId"
             , level = CrmPermissionLevelEnum.OWNER)
     public CommonResult<Boolean> addPermission(@Valid @RequestBody CrmPermissionCreateReqVO reqVO) {
-        // 2. 加入成员
-        crmPermissionService.createPermission(CrmPermissionConvert.INSTANCE.convert(reqVO));
+        permissionService.createPermission(CrmPermissionConvert.INSTANCE.convert(reqVO));
         return success(true);
     }
 
+    @PutMapping("/receive")
+    @Operation(summary = "领取公海数据")
+    @PreAuthorize("@ss.hasPermission('crm:permission:update')")
+    public CommonResult<Boolean> receive(@RequestParam("bizType") Integer bizType, @RequestParam("bizId") Long bizId) {
+        permissionService.receiveBiz(bizType, bizId, getLoginUserId());
+        return success(true);
+    }
+
+    @PutMapping("/put-pool")
+    @Operation(summary = "数据放入公海")
+    @PreAuthorize("@ss.hasPermission('crm:permission:update')")
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#bizType", bizId = "#bizId"
+            , level = CrmPermissionLevelEnum.OWNER)
+    public CommonResult<Boolean> putPool(@RequestParam(value = "bizType") Integer bizType, @RequestParam("bizId") Long bizId) {
+        permissionService.putPool(bizType, bizId, getLoginUserId());
+        return success(true);
+    }
 
     @PutMapping("/update")
     @Operation(summary = "编辑团队成员")
@@ -71,7 +90,7 @@ public class CrmPermissionController {
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#updateReqVO.bizType", bizId = "#updateReqVO.bizId"
             , level = CrmPermissionLevelEnum.WRITE)
     public CommonResult<Boolean> updatePermission(@Valid @RequestBody CrmPermissionUpdateReqVO updateReqVO) {
-        crmPermissionService.updatePermission(CrmPermissionConvert.INSTANCE.convert(updateReqVO));
+        permissionService.updatePermission(CrmPermissionConvert.INSTANCE.convert(updateReqVO));
         return success(true);
     }
 
@@ -88,7 +107,7 @@ public class CrmPermissionController {
     public CommonResult<Boolean> deletePermission(@RequestParam("bizType") Integer bizType,
                                                   @RequestParam("bizId") Long bizId,
                                                   @RequestParam("id") Long id) {
-        crmPermissionService.deletePermission(id);
+        permissionService.deletePermission(id);
         return success(true);
     }
 
@@ -101,12 +120,16 @@ public class CrmPermissionController {
     @PreAuthorize("@ss.hasPermission('crm:permission:query')")
     public CommonResult<List<CrmPermissionRespVO>> getPermissionList(@RequestParam("bizType") Integer bizType,
                                                                      @RequestParam("bizId") Long bizId) {
-        List<CrmPermissionDO> permission = crmPermissionService.getPermissionByBizTypeAndBizId(bizType, bizId);
+        List<CrmPermissionDO> permission = permissionService.getPermissionByBizTypeAndBizId(bizType, bizId);
         if (CollUtil.isEmpty(permission)) {
             return success(Collections.emptyList());
         }
         // TODO @puhui999:池子的逻辑;
-        permission.removeIf(item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID)); // 排除
+        // 判断是否是公海数据
+        Predicate<CrmPermissionDO> filter = item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID);
+        if (anyMatch(permission, filter)) {
+            permission.removeIf(filter); // 排除
+        }
 
         // 拼接数据
         List<AdminUserRespDTO> userList = adminUserApi.getUserList(convertSet(permission, CrmPermissionDO::getUserId));
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index 9bb424305..8fbfafbe2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -85,4 +85,22 @@ public interface CrmPermissionService {
      */
     List<CrmPermissionDO> getPermissionListByBizTypeAndUserId(Integer bizType, Long userId);
 
+    /**
+     * 领取公海数据
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @param userId  用户编号,AdminUser#id
+     */
+    void receiveBiz(Integer bizType, Long bizId, Long userId);
+
+    /**
+     * 数据放入公海
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @param userId  用户编号,AdminUser#id
+     */
+    void putPool(Integer bizType, Long bizId, Long userId);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index b24ae4156..a87608e1f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -141,4 +141,24 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         return crmPermissionMapper.selectListByBizTypeAndUserId(bizType, userId);
     }
 
+    @Override
+    public void receiveBiz(Integer bizType, Long bizId, Long userId) {
+        CrmPermissionDO permission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(bizType, bizId, CrmPermissionDO.POOL_USER_ID);
+        if (permission == null) { // 不存在则模块数据也不存在
+            throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, CrmBizTypeEnum.getNameByType(bizType));
+        }
+
+        crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId()).setUserId(userId));
+    }
+
+    @Override
+    public void putPool(Integer bizType, Long bizId, Long userId) {
+        CrmPermissionDO permission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(bizType, bizId, userId);
+        if (permission == null) { // 不存在则模块数据也不存在
+            throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, CrmBizTypeEnum.getNameByType(bizType));
+        }
+
+        crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId()).setUserId(CrmPermissionDO.POOL_USER_ID));
+    }
+
 }

From f4c92089e58c50e4bbba545bac8927b13fd50117 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Wed, 8 Nov 2023 16:42:22 +0800
Subject: [PATCH 080/101] =?UTF-8?q?CRM-=E5=AE=A2=E6=88=B7=EF=BC=9A?=
 =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=88=86=E9=A1=B5=E6=95=B0=E6=8D=AE=E5=9C=BA?=
 =?UTF-8?q?=E6=99=AF=E8=BF=87=E6=BB=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../enums/customer/CrmCustomerSceneEnum.java  | 46 +++++++++++++++++++
 .../customer/vo/CrmCustomerPageReqVO.java     |  8 +++-
 .../core/aop/CrmPermissionAspect.java         |  6 +--
 .../customer/CrmCustomerServiceImpl.java      |  6 +++
 4 files changed, 62 insertions(+), 4 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java
new file mode 100644
index 000000000..1bb374081
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.crm.enums.customer;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * CRM 客户等级
+ *
+ * @author Wanwan
+ */
+@Getter
+@AllArgsConstructor
+public enum CrmCustomerSceneEnum implements IntArrayValuable {
+
+    OWNER(1, "我负责的客户"),
+    FOLLOW(2, "我关注的客户");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerSceneEnum::getType).toArray();
+
+    /**
+     * 场景类型
+     */
+    private final Integer type;
+    /**
+     * 场景名称
+     */
+    private final String name;
+
+    public static boolean isOwner(Integer type) {
+        return ObjUtil.equal(OWNER.getType(), type);
+    }
+
+    public static boolean isFollow(Integer type) {
+        return ObjUtil.equal(FOLLOW.getType(), type);
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
index c34980b36..5f8b9f28e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerSceneEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -27,5 +28,10 @@ public class CrmCustomerPageReqVO extends PageParam {
     @Schema(description = "客户来源", example = "1")
     private Integer source;
 
-    // TODO @芋艿:场景;
+    /**
+     * 场景类型,关联 {@link CrmCustomerSceneEnum}
+     */
+    @Schema(description = "场景类型", example = "1")
+    private Integer sceneType;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index c306bd78a..251a921ff 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -59,9 +59,9 @@ public class CrmPermissionAspect {
     @Before("@annotation(crmPermission)")
     public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) throws NoSuchMethodException {
         // TODO 芋艿:临时,方便大家调试
-        //if (true) {
-        //    return;
-        //}
+        if (true) {
+            return;
+        }
         KeyValue<Long, Integer> bizIdAndBizType = getBizIdAndBizType(joinPoint, crmPermission);
         Integer bizType = bizIdAndBizType.getValue(); // 模块类型
         Long bizId = bizIdAndBizType.getKey(); // 模块数据编号
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 56e958334..5d56211d6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -3,11 +3,13 @@ package cn.iocoder.yudao.module.crm.service.customer;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 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.*;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
+import cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerSceneEnum;
 import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
@@ -100,6 +102,10 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         // 1.2 获取当前用户能看的分页数据
         List<CrmPermissionDO> permissions = crmPermissionService.getPermissionListByBizTypeAndUserId(
                 CrmBizTypeEnum.CRM_CUSTOMER.getType(), userId);
+        // 1.3 TODO 场景数据过滤
+        if (CrmCustomerSceneEnum.isOwner(pageReqVO.getSceneType())) { // 场景一:我负责的数据
+            permissions = CollectionUtils.filterList(permissions, item -> CrmPermissionLevelEnum.isOwner(item.getLevel()));
+        }
         Set<Long> ids = convertSet(permissions, CrmPermissionDO::getBizId);
         if (CollUtil.isEmpty(ids)) { // 没得说明没有什么给他看的
             return PageResult.empty();

From 670c0962a76fbf8e393d347c78ae66b2c4af1ee0 Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 17:03:37 +0800
Subject: [PATCH 081/101] =?UTF-8?q?refactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../favorite/ProductFavoriteController.java   | 29 ++-----------------
 1 file changed, 3 insertions(+), 26 deletions(-)

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
index 9157e8a05..6f66c2062 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
@@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.product.controller.admin.favorite;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
 import cn.iocoder.yudao.module.product.convert.favorite.ProductFavoriteConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
@@ -16,7 +14,9 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
@@ -37,21 +37,6 @@ public class ProductFavoriteController {
     @Resource
     private ProductSpuService productSpuService;
 
-    @PostMapping("/create")
-    @Operation(summary = "添加单个商品收藏")
-    @PreAuthorize("@ss.hasPermission('product:favorite:create')")
-    public CommonResult<Long> createFavorite(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
-        return success(productFavoriteService.createFavorite(reqVO.getUserId(), reqVO.getSpuId()));
-    }
-
-    @DeleteMapping("/delete")
-    @Operation(summary = "取消单个商品收藏")
-    @PreAuthorize("@ss.hasPermission('product:favorite:delete')")
-    public CommonResult<Boolean> deleteFavorite(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
-        productFavoriteService.deleteFavorite(reqVO.getUserId(), reqVO.getSpuId());
-        return success(Boolean.TRUE);
-    }
-
     @GetMapping("/page")
     @Operation(summary = "获得商品收藏分页")
     @PreAuthorize("@ss.hasPermission('product:favorite:query')")
@@ -72,12 +57,4 @@ public class ProductFavoriteController {
 
         return success(pageResult);
     }
-
-    @PostMapping(value = "/exits")
-    @Operation(summary = "检查是否收藏过商品")
-    @PreAuthenticated
-    public CommonResult<Boolean> isFavoriteExists(@Valid @RequestBody ProductFavoriteReqVO reqVO) {
-        ProductFavoriteDO favorite = productFavoriteService.getFavorite(reqVO.getUserId(), reqVO.getSpuId());
-        return success(favorite != null);
-    }
 }

From 92be763c6f35a1735d45e8fae8cce870c8808c10 Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 17:18:28 +0800
Subject: [PATCH 082/101] =?UTF-8?q?refactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/product/service/favorite/ProductFavoriteService.java  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
index 295af4c94..3dd3bd59e 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteService.java
@@ -60,4 +60,5 @@ public interface ProductFavoriteService {
      * @return 数量
      */
     Long getFavoriteCount(Long userId);
+
 }

From 5a5a42463bbfad1f95a32a6c8ee1c13094f633c3 Mon Sep 17 00:00:00 2001
From: niou233 <2922564446@qq.com>
Date: Wed, 8 Nov 2023 17:22:00 +0800
Subject: [PATCH 083/101] =?UTF-8?q?refactor:=20=E4=BC=9A=E5=91=98=E5=95=86?=
 =?UTF-8?q?=E5=93=81=E6=94=B6=E8=97=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../product/service/favorite/ProductFavoriteServiceImpl.java     | 1 +
 1 file changed, 1 insertion(+)

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
index 945201286..927d030d5 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/favorite/ProductFavoriteServiceImpl.java
@@ -34,6 +34,7 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
         if (favorite != null) {
             throw exception(FAVORITE_EXISTS);
         }
+
         ProductFavoriteDO entity = ProductFavoriteConvert.INSTANCE.convert(userId, spuId);
         productFavoriteMapper.insert(entity);
         return entity.getId();

From 9abff2c7adc92c934ef204d91ec0240d500eb863 Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Wed, 8 Nov 2023 23:16:55 +0800
Subject: [PATCH 084/101] =?UTF-8?q?feat:=20CRM=20=E5=AE=A2=E6=88=B7?=
 =?UTF-8?q?=E5=85=AC=E6=B5=B7=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql      | 35 +++++++++++++++++++++++++++++++++++
 sql/mysql/crm_data.sql |  3 +++
 sql/mysql/crm_menu.sql | 23 +++++++++++++++++++++++
 3 files changed, 61 insertions(+)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index e3e2ca0a0..2392ef482 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -1 +1,36 @@
 SET NAMES utf8mb4;
+
+DROP TABLE IF EXISTS `crm_customer_limit_config`;
+CREATE TABLE `crm_customer_limit_config`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `type` int NOT NULL COMMENT '规则类型 1: 拥有客户数限制,2:锁定客户数限制',
+  `user_ids` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '规则适用人群',
+  `dept_ids` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '规则适用部门',
+  `max_count` int NOT NULL COMMENT '数量上限',
+  `deal_count_enabled` tinyint NULL DEFAULT NULL COMMENT '成交客户是否占有拥有客户数(当 type = 1 时)',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客户限制配置表' ROW_FORMAT = DYNAMIC;
+
+
+DROP TABLE IF EXISTS `crm_customer_pool_config`;
+CREATE TABLE `crm_customer_pool_config`  (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+ `enabled` tinyint(1) NOT NULL COMMENT '是否启用客户公海',
+ `contact_expire_days` int NULL DEFAULT NULL COMMENT '未跟进放入公海天数',
+ `deal_expire_days` int NULL DEFAULT NULL COMMENT '未成交放入公海天数',
+ `notify_enabled` tinyint(1) NULL DEFAULT NULL COMMENT '是否开启提前提醒',
+ `notify_days` int NULL DEFAULT NULL COMMENT '提前提醒天数',
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+ `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客户公海配置表' ROW_FORMAT = DYNAMIC;
\ No newline at end of file
diff --git a/sql/mysql/crm_data.sql b/sql/mysql/crm_data.sql
index b5be1e691..7453f8d25 100644
--- a/sql/mysql/crm_data.sql
+++ b/sql/mysql/crm_data.sql
@@ -18,3 +18,6 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1399, 6, '支付宝', '6', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:38', '1', '2023-10-18 21:55:38', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1400, 7, '微信支付', '7', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:53', '1', '2023-10-18 21:55:53', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1401, 8, '其他', '8', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:56:06', '1', '2023-10-18 21:56:06', b'0');
+
+
+INSERT INTO `crm_customer_pool_config` (`id`, `enabled`, `contact_expire_days`, `deal_expire_days`, `notify_enabled`, `notify_days`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 1, 15, 30, 1, 3, '1', NOW(), '1', NOW(), b'0', 1);
\ No newline at end of file
diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index e69de29bb..a72507544 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -0,0 +1,23 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+   '客户公海配置', '', 2, 0, 2397,
+   'customer-pool-config', 'ep:data-analysis', 'crm/customerPoolConf/index', 0, 'CustomerPoolConf'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户公海配置保存', 'crm:customer-pool-config:update', 3, 1, @parentId,
+   '', '', '', 0
+);
\ No newline at end of file

From 2915b46fa599b1215ed510dfbce93409e5939310 Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Thu, 9 Nov 2023 00:01:43 +0800
Subject: [PATCH 085/101] =?UTF-8?q?feat:=20CRM=20=E5=AE=A2=E6=88=B7?=
 =?UTF-8?q?=E5=85=AC=E6=B5=B7=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../yudao/module/crm/enums/ErrorCodeConstants.java   |  1 +
 .../customer/CrmCustomerPoolConfigServiceImpl.java   | 12 ++++++++++++
 2 files changed, 13 insertions(+)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index 36767f83a..d1f3572db 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -34,6 +34,7 @@ public interface ErrorCodeConstants {
 
     // ========== 客户管理 1_020_006_000 ==========
     ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
+    ErrorCode CUSTOMER_POOL_CONFIG_ERROR = new ErrorCode(1_020_006_001, "客户公海规则设置不正确");
 
     // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
index 5c3099875..1ea6f98b4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.customer;
 
+import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPoolConfigUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
@@ -11,6 +13,9 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import java.util.Objects;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_POOL_CONFIG_ERROR;
+
 /**
  * 客户公海配置 Service 实现类
  *
@@ -39,6 +44,13 @@ public class CrmCustomerPoolConfigServiceImpl implements CrmCustomerPoolConfigSe
      */
     @Override
     public void updateCustomerPoolConfig(CrmCustomerPoolConfigUpdateReqVO saveReqVO) {
+        if (BooleanUtil.isTrue(saveReqVO.getEnabled()) && (ObjectUtil.hasNull(saveReqVO.getContactExpireDays(), saveReqVO.getDealExpireDays()))) {
+            throw exception(CUSTOMER_POOL_CONFIG_ERROR);
+        }
+        if (BooleanUtil.isTrue(saveReqVO.getNotifyEnabled()) && (Objects.isNull(saveReqVO.getNotifyDays()))) {
+            throw exception(CUSTOMER_POOL_CONFIG_ERROR);
+        }
+
         // 存在,则进行更新
         CrmCustomerPoolConfigDO dbConfig = getCustomerPoolConfig();
         if (Objects.nonNull(dbConfig)) {

From 5b295d56b6ebeda635cfaa0ff98afe890e98de98 Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Sat, 11 Nov 2023 20:49:52 +0800
Subject: [PATCH 086/101] =?UTF-8?q?feat:=20CRM=20=E5=AE=A2=E6=88=B7?=
 =?UTF-8?q?=E9=99=90=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm_menu.sql                        |  67 +++++++++-
 .../util/collection/CollectionUtils.java      |  18 +++
 .../module/crm/enums/ErrorCodeConstants.java  |   1 +
 .../CrmCustomerLimitConfigController.java     |  94 ++++++++++++++
 .../vo/CrmCustomerLimitConfigBaseVO.java      |  33 +++++
 .../vo/CrmCustomerLimitConfigCreateReqVO.java |  14 +++
 .../vo/CrmCustomerLimitConfigPageReqVO.java   |  18 +++
 .../vo/CrmCustomerLimitConfigRespVO.java      |  29 +++++
 .../vo/CrmCustomerLimitConfigUpdateReqVO.java |  20 +++
 .../CrmCustomerLimitConfigConvert.java        |  65 ++++++++++
 .../CrmCustomerLimitConfigDO.java             |  56 +++++++++
 .../CrmCustomerLimitConfigMapper.java         |  24 ++++
 .../CrmCustomerLimitConfigService.java        |  56 +++++++++
 .../CrmCustomerLimitConfigServiceImpl.java    |  72 +++++++++++
 .../CrmCustomerLimitConfigMapper.xml          |  12 ++
 ...CrmCustomerLimitConfigServiceImplTest.java | 118 ++++++++++++++++++
 .../src/test/resources/sql/clean.sql          |   4 +-
 .../src/test/resources/sql/create_tables.sql  |  18 ++-
 .../controller/admin/user/UserController.java |  22 ++++
 .../system/service/user/AdminUserService.java |   6 +
 .../service/user/AdminUserServiceImpl.java    |  10 ++
 21 files changed, 754 insertions(+), 3 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigCreateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigPageReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigRespVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigUpdateReqVO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customerlimitconfig/CrmCustomerLimitConfigConvert.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customerlimitconfig/CrmCustomerLimitConfigDO.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customerlimitconfig/CrmCustomerLimitConfigMapper.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigService.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customerlimitconfig/CrmCustomerLimitConfigMapper.xml
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImplTest.java

diff --git a/sql/mysql/crm_menu.sql b/sql/mysql/crm_menu.sql
index a72507544..a4b9105e0 100644
--- a/sql/mysql/crm_menu.sql
+++ b/sql/mysql/crm_menu.sql
@@ -1,3 +1,6 @@
+-- ----------------------------
+-- 客户公海配置
+-- ----------------------------
 -- 菜单 SQL
 INSERT INTO system_menu(
     name, permission, type, sort, parent_id,
@@ -20,4 +23,66 @@ INSERT INTO system_menu(
 VALUES (
    '客户公海配置保存', 'crm:customer-pool-config:update', 3, 1, @parentId,
    '', '', '', 0
-);
\ No newline at end of file
+);
+
+
+
+
+-- ----------------------------
+-- 客户限制配置管理
+-- ----------------------------
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+   '客户限制配置管理', '', 2, 0, 2397,
+   'customer-limit-config', '', 'crm/customerLimitConfig/index', 0, 'CrmCustomerLimitConfig'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置查询', 'crm:customer-limit-config:query', 3, 1, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置创建', 'crm:customer-limit-config:create', 3, 2, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置更新', 'crm:customer-limit-config:update', 3, 3, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置删除', 'crm:customer-limit-config:delete', 3, 4, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置导出', 'crm:customer-limit-config:export', 3, 5, @parentId,
+   '', '', '', 0
+);
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
index b8d57df6d..1261258ac 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
@@ -276,6 +276,15 @@ public class CollectionUtils {
         return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
     }
 
+    public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from,
+                                                         Function<? super T, ? extends U> mapper,
+                                                         Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().map(mapper).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
     public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from,
                                                     Function<T, ? extends Stream<? extends U>> func) {
         if (CollUtil.isEmpty(from)) {
@@ -284,4 +293,13 @@ public class CollectionUtils {
         return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
     }
 
+    public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from,
+                                                       Function<? super T, ? extends U> mapper,
+                                                       Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(mapper).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index d1f3572db..c401a1905 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -35,6 +35,7 @@ public interface ErrorCodeConstants {
     // ========== 客户管理 1_020_006_000 ==========
     ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
     ErrorCode CUSTOMER_POOL_CONFIG_ERROR = new ErrorCode(1_020_006_001, "客户公海规则设置不正确");
+    ErrorCode CUSTOMER_LIMIT_CONFIG_NOT_EXISTS = new ErrorCode(1_020_006_002, "客户限制配置不存在");
 
     // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
new file mode 100644
index 000000000..80f6e461b
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.customerlimitconfig.CrmCustomerLimitConfigConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
+import cn.iocoder.yudao.module.crm.service.customerlimitconfig.CrmCustomerLimitConfigService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 客户限制配置")
+@RestController
+@RequestMapping("/crm/customer-limit-config")
+@Validated
+public class CrmCustomerLimitConfigController {
+
+    @Resource
+    private CrmCustomerLimitConfigService customerLimitConfigService;
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建客户限制配置")
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:create')")
+    public CommonResult<Long> createCustomerLimitConfig(@Valid @RequestBody CrmCustomerLimitConfigCreateReqVO createReqVO) {
+        return success(customerLimitConfigService.createCustomerLimitConfig(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新客户限制配置")
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:update')")
+    public CommonResult<Boolean> updateCustomerLimitConfig(@Valid @RequestBody CrmCustomerLimitConfigUpdateReqVO updateReqVO) {
+        customerLimitConfigService.updateCustomerLimitConfig(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除客户限制配置")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:delete')")
+    public CommonResult<Boolean> deleteCustomerLimitConfig(@RequestParam("id") Long id) {
+        customerLimitConfigService.deleteCustomerLimitConfig(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得客户限制配置")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:query')")
+    public CommonResult<CrmCustomerLimitConfigRespVO> getCustomerLimitConfig(@RequestParam("id") Long id) {
+        CrmCustomerLimitConfigDO customerLimitConfig = customerLimitConfigService.getCustomerLimitConfig(id);
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(new HashSet<>(customerLimitConfig.getUserIds()));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(new HashSet<>(customerLimitConfig.getDeptIds()));
+        return success(CrmCustomerLimitConfigConvert.INSTANCE.convert(customerLimitConfig, userMap, deptMap));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得客户限制配置分页")
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:query')")
+    public CommonResult<PageResult<CrmCustomerLimitConfigRespVO>> getCustomerLimitConfigPage(@Valid CrmCustomerLimitConfigPageReqVO pageVO) {
+        PageResult<CrmCustomerLimitConfigDO> pageResult = customerLimitConfigService.getCustomerLimitConfigPage(pageVO);
+        Set<Long> userIds = CollectionUtils.convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getUserIds, Collection::stream);
+        Set<Long> deptIds = CollectionUtils.convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getDeptIds, Collection::stream);
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(deptIds);
+        return success(CrmCustomerLimitConfigConvert.INSTANCE.convertPage(pageResult, userMap, deptMap));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java
new file mode 100644
index 000000000..f429dc12e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java
@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 客户限制配置 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmCustomerLimitConfigBaseVO {
+
+    @Schema(description = "规则类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "规则类型不能为空")
+    private Integer type;
+
+    @Schema(description = "规则适用人群")
+    private List<Long> userIds;
+
+    @Schema(description = "规则适用部门")
+    private List<Long> deptIds;
+
+    @Schema(description = "数量上限", requiredMode = Schema.RequiredMode.REQUIRED, example = "28384")
+    @NotNull(message = "数量上限不能为空")
+    private Integer maxCount;
+
+    @Schema(description = "成交客户是否占有拥有客户数(当 type = 1 时)")
+    private Boolean dealCountEnabled;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigCreateReqVO.java
new file mode 100644
index 000000000..cb6688297
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 客户限制配置创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerLimitConfigCreateReqVO extends CrmCustomerLimitConfigBaseVO {
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigPageReqVO.java
new file mode 100644
index 000000000..fb913d196
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigPageReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.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;
+
+@Schema(description = "管理后台 - 客户限制配置分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerLimitConfigPageReqVO extends PageParam {
+
+    @Schema(description = "规则类型", example = "1")
+    private Integer type;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigRespVO.java
new file mode 100644
index 000000000..7be29c549
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigRespVO.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 客户限制配置 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerLimitConfigRespVO extends CrmCustomerLimitConfigBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27930")
+    private Long id;
+
+    @Schema(description = "规则适用人群名称")
+    private String userNames;
+
+    @Schema(description = "规则适用部门名称")
+    private String deptNames;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigUpdateReqVO.java
new file mode 100644
index 000000000..038d8f45d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigUpdateReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 客户限制配置更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerLimitConfigUpdateReqVO extends CrmCustomerLimitConfigBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27930")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customerlimitconfig/CrmCustomerLimitConfigConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customerlimitconfig/CrmCustomerLimitConfigConvert.java
new file mode 100644
index 000000000..8c8d02b84
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customerlimitconfig/CrmCustomerLimitConfigConvert.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.crm.convert.customerlimitconfig;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.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;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 客户限制配置 Convert
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmCustomerLimitConfigConvert {
+
+    CrmCustomerLimitConfigConvert INSTANCE = Mappers.getMapper(CrmCustomerLimitConfigConvert.class);
+
+    CrmCustomerLimitConfigDO convert(CrmCustomerLimitConfigCreateReqVO bean);
+
+    CrmCustomerLimitConfigDO convert(CrmCustomerLimitConfigUpdateReqVO bean);
+
+    CrmCustomerLimitConfigRespVO convert(CrmCustomerLimitConfigDO bean);
+
+    List<CrmCustomerLimitConfigRespVO> convertList(List<CrmCustomerLimitConfigDO> list);
+
+    PageResult<CrmCustomerLimitConfigRespVO> convertPage(PageResult<CrmCustomerLimitConfigDO> page);
+
+    default PageResult<CrmCustomerLimitConfigRespVO> convertPage(PageResult<CrmCustomerLimitConfigDO> pageResult,
+                                                                 Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
+        PageResult<CrmCustomerLimitConfigRespVO> result = convertPage(pageResult);
+        result.getList().forEach(respVo -> fillNameField(userMap, deptMap, respVo));
+        return result;
+    }
+
+    default CrmCustomerLimitConfigRespVO convert(CrmCustomerLimitConfigDO customerLimitConfig,
+                                                 Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
+        CrmCustomerLimitConfigRespVO respVo = convert(customerLimitConfig);
+        fillNameField(userMap, deptMap, respVo);
+        return respVo;
+    }
+
+    /**
+     * 填充名称字段
+     *
+     * @param userMap 用户映射
+     * @param deptMap 部门映射
+     * @param respVo 响应实体
+     */
+    static void fillNameField(Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap, CrmCustomerLimitConfigRespVO respVo) {
+        respVo.setUserNames(respVo.getUserIds().stream().map(userMap::get)
+                .filter(Objects::nonNull).map(AdminUserRespDTO::getNickname).collect(Collectors.joining(",")));
+        respVo.setDeptNames(respVo.getDeptIds().stream().map(deptMap::get)
+                .filter(Objects::nonNull).map(DeptRespDTO::getName).collect(Collectors.joining(",")));
+    }
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customerlimitconfig/CrmCustomerLimitConfigDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customerlimitconfig/CrmCustomerLimitConfigDO.java
new file mode 100644
index 000000000..d5ed80d7a
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customerlimitconfig/CrmCustomerLimitConfigDO.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
+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.*;
+
+import java.util.List;
+
+/**
+ * 客户限制配置 DO
+ *
+ * @author Wanwan
+ */
+@TableName(value = "crm_customer_limit_config", autoResultMap = true)
+@KeySequence("crm_customer_limit_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmCustomerLimitConfigDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 规则类型
+     */
+    private Integer type;
+    /**
+     * 规则适用人群
+     */
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> userIds;
+    /**
+     * 规则适用部门
+     */
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> deptIds;
+    /**
+     * 数量上限
+     */
+    private Integer maxCount;
+    /**
+     * 成交客户是否占有拥有客户数(当 type = 1 时)
+     */
+    private Boolean dealCountEnabled;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customerlimitconfig/CrmCustomerLimitConfigMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customerlimitconfig/CrmCustomerLimitConfigMapper.java
new file mode 100644
index 000000000..95d5bcdbc
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customerlimitconfig/CrmCustomerLimitConfigMapper.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.customerlimitconfig;
+
+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.customer.vo.CrmCustomerLimitConfigPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 客户限制配置 Mapper
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLimitConfigDO> {
+
+    default PageResult<CrmCustomerLimitConfigDO> selectPage(CrmCustomerLimitConfigPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
+                .eqIfPresent(CrmCustomerLimitConfigDO::getType, reqVO.getType())
+                .orderByDesc(CrmCustomerLimitConfigDO::getId));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigService.java
new file mode 100644
index 000000000..655a0c202
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigService.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.crm.service.customerlimitconfig;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
+
+import javax.validation.Valid;
+
+/**
+ * 客户限制配置 Service 接口
+ *
+ * @author Wanwan
+ */
+public interface CrmCustomerLimitConfigService {
+
+    /**
+     * 创建客户限制配置
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createCustomerLimitConfig(@Valid CrmCustomerLimitConfigCreateReqVO createReqVO);
+
+    /**
+     * 更新客户限制配置
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateCustomerLimitConfig(@Valid CrmCustomerLimitConfigUpdateReqVO updateReqVO);
+
+    /**
+     * 删除客户限制配置
+     *
+     * @param id 编号
+     */
+    void deleteCustomerLimitConfig(Long id);
+
+    /**
+     * 获得客户限制配置
+     *
+     * @param id 编号
+     * @return 客户限制配置
+     */
+    CrmCustomerLimitConfigDO getCustomerLimitConfig(Long id);
+
+    /**
+     * 获得客户限制配置分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 客户限制配置分页
+     */
+    PageResult<CrmCustomerLimitConfigDO> getCustomerLimitConfigPage(CrmCustomerLimitConfigPageReqVO pageReqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
new file mode 100644
index 000000000..101aa63e0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.crm.service.customerlimitconfig;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.customerlimitconfig.CrmCustomerLimitConfigConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.customerlimitconfig.CrmCustomerLimitConfigMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_LIMIT_CONFIG_NOT_EXISTS;
+
+/**
+ * 客户限制配置 Service 实现类
+ *
+ * @author Wanwan
+ */
+@Service
+@Validated
+public class CrmCustomerLimitConfigServiceImpl implements CrmCustomerLimitConfigService {
+
+    @Resource
+    private CrmCustomerLimitConfigMapper customerLimitConfigMapper;
+
+    @Override
+    public Long createCustomerLimitConfig(CrmCustomerLimitConfigCreateReqVO createReqVO) {
+        // 插入
+        CrmCustomerLimitConfigDO customerLimitConfig = CrmCustomerLimitConfigConvert.INSTANCE.convert(createReqVO);
+        customerLimitConfigMapper.insert(customerLimitConfig);
+        // 返回
+        return customerLimitConfig.getId();
+    }
+
+    @Override
+    public void updateCustomerLimitConfig(CrmCustomerLimitConfigUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateCustomerLimitConfigExists(updateReqVO.getId());
+        // 更新
+        CrmCustomerLimitConfigDO updateObj = CrmCustomerLimitConfigConvert.INSTANCE.convert(updateReqVO);
+        customerLimitConfigMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteCustomerLimitConfig(Long id) {
+        // 校验存在
+        validateCustomerLimitConfigExists(id);
+        // 删除
+        customerLimitConfigMapper.deleteById(id);
+    }
+
+    @Override
+    public CrmCustomerLimitConfigDO getCustomerLimitConfig(Long id) {
+        return customerLimitConfigMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<CrmCustomerLimitConfigDO> getCustomerLimitConfigPage(CrmCustomerLimitConfigPageReqVO pageReqVO) {
+        return customerLimitConfigMapper.selectPage(pageReqVO);
+    }
+
+    private void validateCustomerLimitConfigExists(Long id) {
+        if (customerLimitConfigMapper.selectById(id) == null) {
+            throw exception(CUSTOMER_LIMIT_CONFIG_NOT_EXISTS);
+        }
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customerlimitconfig/CrmCustomerLimitConfigMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customerlimitconfig/CrmCustomerLimitConfigMapper.xml
new file mode 100644
index 000000000..c9eefc4e5
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customerlimitconfig/CrmCustomerLimitConfigMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.customerlimitconfig.CrmCustomerLimitConfigMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImplTest.java
new file mode 100644
index 000000000..95fd8ceeb
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImplTest.java
@@ -0,0 +1,118 @@
+package cn.iocoder.yudao.module.crm.service.customerlimitconfig;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.customerlimitconfig.CrmCustomerLimitConfigMapper;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+
+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;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_LIMIT_CONFIG_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link CrmCustomerLimitConfigServiceImpl} 的单元测试类
+ *
+ * @author Wanwan
+ */
+@Import(CrmCustomerLimitConfigServiceImpl.class)
+public class CrmCustomerLimitConfigServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmCustomerLimitConfigServiceImpl customerLimitConfigService;
+
+    @Resource
+    private CrmCustomerLimitConfigMapper customerLimitConfigMapper;
+
+    @Test
+    public void testCreateCustomerLimitConfig_success() {
+        // 准备参数
+        CrmCustomerLimitConfigCreateReqVO reqVO = randomPojo(CrmCustomerLimitConfigCreateReqVO.class);
+
+        // 调用
+        Long customerLimitConfigId = customerLimitConfigService.createCustomerLimitConfig(reqVO);
+        // 断言
+        assertNotNull(customerLimitConfigId);
+        // 校验记录的属性是否正确
+        CrmCustomerLimitConfigDO customerLimitConfig = customerLimitConfigMapper.selectById(customerLimitConfigId);
+        assertPojoEquals(reqVO, customerLimitConfig);
+    }
+
+    @Test
+    public void testUpdateCustomerLimitConfig_success() {
+        // mock 数据
+        CrmCustomerLimitConfigDO dbCustomerLimitConfig = randomPojo(CrmCustomerLimitConfigDO.class);
+        customerLimitConfigMapper.insert(dbCustomerLimitConfig);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmCustomerLimitConfigUpdateReqVO reqVO = randomPojo(CrmCustomerLimitConfigUpdateReqVO.class, o -> {
+            o.setId(dbCustomerLimitConfig.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        customerLimitConfigService.updateCustomerLimitConfig(reqVO);
+        // 校验是否更新正确
+        CrmCustomerLimitConfigDO customerLimitConfig = customerLimitConfigMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, customerLimitConfig);
+    }
+
+    @Test
+    public void testUpdateCustomerLimitConfig_notExists() {
+        // 准备参数
+        CrmCustomerLimitConfigUpdateReqVO reqVO = randomPojo(CrmCustomerLimitConfigUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> customerLimitConfigService.updateCustomerLimitConfig(reqVO), CUSTOMER_LIMIT_CONFIG_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteCustomerLimitConfig_success() {
+        // mock 数据
+        CrmCustomerLimitConfigDO dbCustomerLimitConfig = randomPojo(CrmCustomerLimitConfigDO.class);
+        customerLimitConfigMapper.insert(dbCustomerLimitConfig);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbCustomerLimitConfig.getId();
+
+        // 调用
+        customerLimitConfigService.deleteCustomerLimitConfig(id);
+        // 校验数据不存在了
+        assertNull(customerLimitConfigMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteCustomerLimitConfig_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> customerLimitConfigService.deleteCustomerLimitConfig(id), CUSTOMER_LIMIT_CONFIG_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetCustomerLimitConfigPage() {
+        // mock 数据
+        CrmCustomerLimitConfigDO dbCustomerLimitConfig = randomPojo(CrmCustomerLimitConfigDO.class, o -> { // 等会查询到
+        });
+        customerLimitConfigMapper.insert(dbCustomerLimitConfig);
+        // 准备参数
+        CrmCustomerLimitConfigPageReqVO reqVO = new CrmCustomerLimitConfigPageReqVO();
+
+        // 调用
+        PageResult<CrmCustomerLimitConfigDO> pageResult = customerLimitConfigService.getCustomerLimitConfigPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbCustomerLimitConfig, pageResult.getList().get(0));
+    }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
index c86e5a113..138780eed 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
@@ -6,4 +6,6 @@ DELETE FROM "crm_receivable";
 
 DELETE FROM "crm_receivable_plan";
 
-DELETE FROM "crm_customer";
\ No newline at end of file
+DELETE FROM "crm_customer";
+
+DELETE FROM "crm_customer_limit_config";
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
index 124edb883..f94600db0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -122,4 +122,20 @@ CREATE TABLE IF NOT EXISTS "crm_customer" (
   "deleted" bit NOT NULL DEFAULT FALSE,
   "tenant_id" bigint NOT NULL,
   PRIMARY KEY ("id")
-) COMMENT '客户表';
\ No newline at end of file
+) COMMENT '客户表';
+
+CREATE TABLE IF NOT EXISTS "crm_customer_limit_config" (
+   "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+   "type" int NOT NULL,
+   "user_ids" varchar,
+   "dept_ids" varchar,
+   "max_count" int NOT NULL,
+   "deal_count_enabled" varchar,
+   "creator" varchar DEFAULT '',
+   "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+   "updater" varchar DEFAULT '',
+   "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+   "deleted" bit NOT NULL DEFAULT FALSE,
+   "tenant_id" bigint NOT NULL,
+   PRIMARY KEY ("id")
+) COMMENT '客户限制配置表';
\ No newline at end of file
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
index 549982e3c..03644b30f 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
@@ -109,6 +109,28 @@ public class UserController {
         return success(new PageResult<>(userList, pageResult.getTotal()));
     }
 
+    @GetMapping("/all")
+    @Operation(summary = "查询所有用户列表")
+    public CommonResult<List<UserPageItemRespVO>> getAllUser() {
+        // 获得用户分页列表
+        List<AdminUserDO> pageResult = userService.getUserList();
+        if (CollUtil.isEmpty(pageResult)) {
+            return success(Collections.emptyList()); // 返回空
+        }
+
+        // 获得拼接需要的数据
+        Collection<Long> deptIds = convertList(pageResult, AdminUserDO::getDeptId);
+        Map<Long, DeptDO> deptMap = deptService.getDeptMap(deptIds);
+        // 拼接结果返回
+        List<UserPageItemRespVO> userList = new ArrayList<>(pageResult.size());
+        pageResult.forEach(user -> {
+            UserPageItemRespVO respVO = UserConvert.INSTANCE.convert(user);
+            respVO.setDept(UserConvert.INSTANCE.convert(deptMap.get(user.getDeptId())));
+            userList.add(respVO);
+        });
+        return success(userList);
+    }
+
     @GetMapping("/list-all-simple")
     @Operation(summary = "获取用户精简信息列表", description = "只包含被开启的用户,主要用于前端的下拉选项")
     public CommonResult<List<UserSimpleRespVO>> getSimpleUserList() {
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java
index e10b9e997..569969d51 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java
@@ -209,4 +209,10 @@ public interface AdminUserService {
      */
     boolean isPasswordMatch(String rawPassword, String encodedPassword);
 
+    /**
+     * 获取所有用户列表
+     *
+     * @return 用户列表
+     */
+    List<AdminUserDO> getUserList();
 }
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
index dbfc02ed3..a8b753da8 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
@@ -443,6 +443,16 @@ public class AdminUserServiceImpl implements AdminUserService {
         return passwordEncoder.matches(rawPassword, encodedPassword);
     }
 
+    /**
+     * 获取所有用户列表
+     *
+     * @return 用户列表
+     */
+    @Override
+    public List<AdminUserDO> getUserList() {
+        return userMapper.selectList();
+    }
+
     /**
      * 对密码进行加密
      *

From 7598ed05cf6ebd9085473554b977570525f92a04 Mon Sep 17 00:00:00 2001
From: zyna <chenjidemenglin20@126.com>
Date: Sat, 11 Nov 2023 23:31:09 +0800
Subject: [PATCH 087/101] =?UTF-8?q?crm=E8=81=94=E7=B3=BB=E4=BA=BA=E7=AC=AC?=
 =?UTF-8?q?=E4=BA=8C=E7=89=88=E6=96=B0=E5=A2=9E=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/contact/ContactController.java      | 93 ++++++++++++-------
 .../admin/contact/vo/ContactBaseVO.java       | 55 ++++++-----
 .../admin/contact/vo/ContactCreateReqVO.java  |  8 +-
 .../admin/contact/vo/ContactExcelVO.java      | 41 +++++---
 .../admin/contact/vo/ContactExportReqVO.java  | 37 ++++++--
 .../admin/contact/vo/ContactPageReqVO.java    | 49 ++++++----
 .../admin/contact/vo/ContactRespVO.java       | 12 ++-
 .../admin/contact/vo/ContactSimpleRespVO.java | 20 ++++
 .../admin/contact/vo/ContactUpdateReqVO.java  | 11 ++-
 .../crm/convert/contact/ContactConvert.java   | 21 ++---
 .../crm/dal/dataobject/contact/ContactDO.java | 64 +++++++++----
 .../crm/dal/mysql/contact/ContactMapper.java  | 29 ++++--
 .../crm/service/contact/ContactService.java   | 20 ++--
 .../service/contact/ContactServiceImpl.java   | 71 ++++----------
 14 files changed, 315 insertions(+), 216 deletions(-)
 create mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
index 5d92abc45..d1e0c5ba5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -1,32 +1,43 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact;
 
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-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.contact.vo.*;
-import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.module.crm.service.contact.ContactService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.validation.annotation.Validated;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.NumberUtil;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerExportReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import com.google.common.collect.Lists;
 import org.springframework.web.bind.annotation.*;
-
 import javax.annotation.Resource;
-import javax.servlet.http.HttpServletResponse;
-import javax.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
 import java.io.IOException;
-import java.util.Collection;
-import java.util.List;
+import java.util.stream.Collectors;
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
-import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
-@Tag(name = "管理后台 - CRM 联系人")
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.service.contact.ContactService;
+
+@Tag(name = "管理后台 - crm联系人")
 @RestController
 @RequestMapping("/crm/contact")
 @Validated
@@ -34,12 +45,16 @@ public class ContactController {
 
     @Resource
     private ContactService contactService;
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private CrmCustomerService crmCustomerService;
 
     @PostMapping("/create")
     @Operation(summary = "创建crm联系人")
     @PreAuthorize("@ss.hasPermission('crm:contact:create')")
     public CommonResult<Long> createContact(@Valid @RequestBody ContactCreateReqVO createReqVO) {
-        return success(contactService.createContact(createReqVO, getLoginUserId()));
+        return success(contactService.createContact(createReqVO));
     }
 
     @PutMapping("/update")
@@ -65,7 +80,12 @@ public class ContactController {
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<ContactRespVO> getContact(@RequestParam("id") Long id) {
         ContactDO contact = contactService.getContact(id);
-        return success(ContactConvert.INSTANCE.convert(contact));
+        ContactRespVO contactRespVO  = ContactConvert.INSTANCE.convert(contact);
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(CollUtil.removeNull(Lists.newArrayList(
+                NumberUtil.parseLong(contact.getCreator()))));
+        contactRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(contact.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
+        contactRespVO.setCustomerName(Optional.ofNullable(crmCustomerService.getCustomer(contact.getCustomerId())).map(CrmCustomerDO::getName).orElse(null));
+        return success(contactRespVO);
     }
 
     @GetMapping("/list")
@@ -76,13 +96,26 @@ public class ContactController {
         List<ContactDO> list = contactService.getContactList(ids);
         return success(ContactConvert.INSTANCE.convertList(list));
     }
-
+    @GetMapping("/simpleAlllist")
+    @Operation(summary = "获得crm联系人列表")
+    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
+    public CommonResult<List<ContactSimpleRespVO>> simpleAlllist() {
+        List<ContactDO> list = contactService.allContactList();
+        return success(ContactConvert.INSTANCE.convertAllList(list));
+    }
     @GetMapping("/page")
     @Operation(summary = "获得crm联系人分页")
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<PageResult<ContactRespVO>> getContactPage(@Valid ContactPageReqVO pageVO) {
-        PageResult<ContactDO> pageResult = contactService.getContactPage(pageVO);
-        return success(ContactConvert.INSTANCE.convertPage(pageResult));
+        PageResult<ContactDO> pageData = contactService.getContactPage(pageVO);
+        PageResult<ContactRespVO> pageResult =ContactConvert.INSTANCE.convertPage(pageData);
+        //待接口实现后修改
+        List<CrmCustomerDO> crmCustomerDOList = crmCustomerService.getCustomerList(new CrmCustomerExportReqVO());
+        Map<Long,CrmCustomerDO> crmCustomerDOMap = crmCustomerDOList.stream().collect(Collectors.toMap(CrmCustomerDO::getId,v->v));
+        pageResult.getList().forEach(item -> {
+            item.setCustomerName(Optional.ofNullable(crmCustomerDOMap.get(item.getCustomerId())).map(CrmCustomerDO::getName).orElse(null));
+        });
+        return success(pageResult);
     }
 
     @GetMapping("/export-excel")
@@ -97,12 +130,4 @@ public class ContactController {
         ExcelUtils.write(response, "crm联系人.xls", "数据", ContactExcelVO.class, datas);
     }
 
-    @PutMapping("/transfer")
-    @Operation(summary = "联系人转移")
-    @PreAuthorize("@ss.hasPermission('crm:contact:update')")
-    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContactTransferReqVO reqVO) {
-        contactService.transferContact(reqVO, getLoginUserId());
-        return success(true);
-    }
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
index 0f9da5c06..6a75dbf2e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
@@ -1,12 +1,17 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
+import lombok.*;
+
+import java.time.LocalDate;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import javax.validation.constraints.*;
 import org.springframework.format.annotation.DateTimeFormat;
 
-import javax.validation.constraints.NotNull;
-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;
 
 /**
@@ -16,31 +21,19 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @Data
 public class ContactBaseVO {
 
-    // TODO @zyna:部分字段,缺少 example,需要补充;
-
-    @Schema(description = "联系人名称", example = "张三")
-    @NotNull(message = "姓名不能为空")
-    private String name;
-
     @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
     private LocalDateTime nextTime;
 
-    // TODO @zyna:缺少 validator 的校验
     @Schema(description = "手机号")
     private String mobile;
 
-    // TODO @zyna:缺少 validator 的校验
     @Schema(description = "电话")
     private String telephone;
 
-    // TODO @zyna:缺少 validator 的校验
     @Schema(description = "电子邮箱")
     private String email;
 
-    @Schema(description = "职务")
-    private String post;
-
-    // TODO @zyna:非空校验
     @Schema(description = "客户编号", example = "10795")
     private Long customerId;
 
@@ -50,12 +43,32 @@ public class ContactBaseVO {
     @Schema(description = "备注", example = "你说的对")
     private String remark;
 
-    // TODO @zyna:这个新建的时候,应该不会传递;而是后端默认设置自己为负责人;
-    @Schema(description = "负责人用户编号", example = "7648")
-    private Long ownerUserId;
-
     @Schema(description = "最后跟进时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime lastTime;
 
+    @Schema(description = "直属上级", example = "23457")
+    private Long parentId;
+
+    @Schema(description = "姓名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "职位")
+    private String post;
+
+    @Schema(description = "QQ")
+    private Long qq;
+
+    @Schema(description = "微信")
+    private String webchat;
+
+    @Schema(description = "性别")
+    private Integer sex;
+
+    @Schema(description = "是否关键决策人")
+    private Boolean policyMakers;
+
+    @Schema(description = "负责人用户编号", example = "14334")
+    private String ownerUserId;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
index 5eccfea74..424d945dc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
@@ -1,11 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
+import lombok.*;
+import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
+import javax.validation.constraints.*;
 
-@Schema(description = "管理后台 - CRM 联系人创建 Request VO")
+@Schema(description = "管理后台 - crm联系人创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
index 3ac5f3765..f7ca2b8d0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
+
+import java.time.LocalDate;
 import java.util.*;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
@@ -17,12 +19,6 @@ import com.alibaba.excel.annotation.ExcelProperty;
 @Data
 public class ContactExcelVO {
 
-    @ExcelProperty("主键")
-    private Long id;
-
-    @ExcelProperty("联系人名称")
-    private String name;
-
     @ExcelProperty("下次联系时间")
     private LocalDateTime nextTime;
 
@@ -35,9 +31,6 @@ public class ContactExcelVO {
     @ExcelProperty("电子邮箱")
     private String email;
 
-    @ExcelProperty("职务")
-    private String post;
-
     @ExcelProperty("客户编号")
     private Long customerId;
 
@@ -47,13 +40,37 @@ public class ContactExcelVO {
     @ExcelProperty("备注")
     private String remark;
 
-    @ExcelProperty("负责人用户编号")
-    private Long ownerUserId;
-
     @ExcelProperty("创建时间")
     private LocalDateTime createTime;
 
     @ExcelProperty("最后跟进时间")
     private LocalDateTime lastTime;
 
+    @ExcelProperty("主键")
+    private Long id;
+
+    @ExcelProperty("直属上级")
+    private Long parentId;
+
+    @ExcelProperty("姓名")
+    private String name;
+
+    @ExcelProperty("职位")
+    private String post;
+
+    @ExcelProperty("QQ")
+    private Long qq;
+
+    @ExcelProperty("微信")
+    private String webchat;
+
+    @ExcelProperty("性别")
+    private Integer sex;
+
+    @ExcelProperty("是否关键决策人")
+    private Boolean policyMakers;
+
+    @ExcelProperty("负责人用户编号")
+    private String ownerUserId;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
index 961cebd0c..180ddb6bd 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import lombok.*;
+
+import java.time.LocalDate;
 import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -9,13 +11,10 @@ import org.springframework.format.annotation.DateTimeFormat;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - CRM 联系人 Excel 导出 Request VO,参数和 ContactPageReqVO 是一致的")
+@Schema(description = "管理后台 - crm联系人 Excel 导出 Request VO,参数和 ContactPageReqVO 是一致的")
 @Data
 public class ContactExportReqVO {
 
-    @Schema(description = "联系人名称", example = "张三")
-    private String name;
-
     @Schema(description = "下次联系时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] nextTime;
@@ -29,9 +28,6 @@ public class ContactExportReqVO {
     @Schema(description = "电子邮箱")
     private String email;
 
-    @Schema(description = "职务")
-    private String post;
-
     @Schema(description = "客户编号", example = "10795")
     private Long customerId;
 
@@ -41,9 +37,6 @@ public class ContactExportReqVO {
     @Schema(description = "备注", example = "你说的对")
     private String remark;
 
-    @Schema(description = "负责人用户编号", example = "7648")
-    private Long ownerUserId;
-
     @Schema(description = "创建时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;
@@ -52,4 +45,28 @@ public class ContactExportReqVO {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] lastTime;
 
+    @Schema(description = "直属上级", example = "23457")
+    private Long parentId;
+
+    @Schema(description = "姓名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "职位")
+    private String post;
+
+    @Schema(description = "QQ")
+    private Long qq;
+
+    @Schema(description = "微信")
+    private String webchat;
+
+    @Schema(description = "性别")
+    private Integer sex;
+
+    @Schema(description = "是否关键决策人")
+    private Boolean policyMakers;
+
+    @Schema(description = "负责人用户编号", example = "14334")
+    private String ownerUserId;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
index 64d8a04b9..7aac9083a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
@@ -1,27 +1,22 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.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 lombok.*;
 
+import java.time.LocalDate;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+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 = "管理后台 - CRM 联系人分页 Request VO")
+@Schema(description = "管理后台 - crm联系人分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class ContactPageReqVO extends PageParam {
 
-    // TODO @芋艿:需要查询的字段;
-
-    @Schema(description = "联系人名称", example = "张三")
-    private String name;
-
     @Schema(description = "下次联系时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] nextTime;
@@ -35,9 +30,6 @@ public class ContactPageReqVO extends PageParam {
     @Schema(description = "电子邮箱")
     private String email;
 
-    @Schema(description = "职务")
-    private String post;
-
     @Schema(description = "客户编号", example = "10795")
     private Long customerId;
 
@@ -47,9 +39,6 @@ public class ContactPageReqVO extends PageParam {
     @Schema(description = "备注", example = "你说的对")
     private String remark;
 
-    @Schema(description = "负责人用户编号", example = "7648")
-    private Long ownerUserId;
-
     @Schema(description = "创建时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;
@@ -58,4 +47,28 @@ public class ContactPageReqVO extends PageParam {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] lastTime;
 
+    @Schema(description = "直属上级", example = "23457")
+    private Long parentId;
+
+    @Schema(description = "姓名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "职位")
+    private String post;
+
+    @Schema(description = "QQ")
+    private Long qq;
+
+    @Schema(description = "微信")
+    private String webchat;
+
+    @Schema(description = "性别")
+    private Integer sex;
+
+    @Schema(description = "是否关键决策人")
+    private Boolean policyMakers;
+
+    @Schema(description = "负责人用户编号", example = "14334")
+    private String ownerUserId;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
index 6a02c69d2..062d9eee5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
@@ -4,17 +4,19 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import java.time.LocalDateTime;
 
-@Schema(description = "管理后台 - CRM 联系人 Response VO")
+@Schema(description = "管理后台 - crm联系人 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class ContactRespVO extends ContactBaseVO {
 
-    @Schema(description = "主键", example = "23210")
-    private Long id;
-
     @Schema(description = "创建时间")
     private LocalDateTime createTime;
     @Schema(description = "创建人")
-    private String creator;
+    private String creatorName;
+    @Schema(description = "客户")
+    private String customerName;
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
+    private Long id;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java
new file mode 100644
index 000000000..dd37802d5
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - crm联系人 Response VO")
+@Data
+@ToString(callSuper = true)
+public class ContactSimpleRespVO {
+    @Schema(description = "姓名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
+    private Long id;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
index d2621fa5e..6aeaaec7c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
@@ -1,17 +1,18 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
 
-@Schema(description = "管理后台 - CRM 联系人更新 Request VO")
+@Schema(description = "管理后台 - crm联系人更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class ContactUpdateReqVO extends ContactBaseVO {
 
-    @Schema(description = "主键", example = "23210")
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
+    @NotNull(message = "主键不能为空")
     private Long id;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index b4cc69963..d0b535a19 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -1,18 +1,16 @@
 package cn.iocoder.yudao.module.crm.convert.contact;
 
+import java.util.*;
+
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmTransferPermissionReqBO;
-import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-import org.mapstruct.Mappings;
-import org.mapstruct.factory.Mappers;
-
-import java.util.List;
 
 /**
- * crm 联系人 Convert
+ * crm联系人 Convert
  *
  * @author 芋道源码
  */
@@ -32,11 +30,6 @@ public interface ContactConvert {
     PageResult<ContactRespVO> convertPage(PageResult<ContactDO> page);
 
     List<ContactExcelVO> convertList02(List<ContactDO> list);
-
-    @Mappings({
-            @Mapping(target = "bizId", source = "reqVO.id"),
-            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
-    })
-    CrmTransferPermissionReqBO convert(CrmContactTransferReqVO reqVO, Long userId);
+    List<ContactSimpleRespVO> convertAllList(List<ContactDO> list);
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
index f958fcd64..513ffba45 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
@@ -1,15 +1,17 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
 
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import com.baomidou.mybatisplus.annotation.KeySequence;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 
+import java.time.LocalDate;
+import java.util.*;
 import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 
 /**
- * crm 联系人 DO
+ * crm联系人 DO
  *
  * @author 芋道源码
  */
@@ -23,15 +25,6 @@ import java.time.LocalDateTime;
 @AllArgsConstructor
 public class ContactDO extends BaseDO {
 
-    /**
-     * 主键
-     */
-    @TableId
-    private Long id;
-    /**
-     * 联系人名称
-     */
-    private String name;
     /**
      * 下次联系时间
      */
@@ -48,14 +41,8 @@ public class ContactDO extends BaseDO {
      * 电子邮箱
      */
     private String email;
-    /**
-     * 职务
-     */
-    private String post;
     /**
      * 客户编号
-     *
-     * TODO @zyna:关联的字段,也要写下
      */
     private Long customerId;
     /**
@@ -70,5 +57,42 @@ public class ContactDO extends BaseDO {
      * 最后跟进时间
      */
     private LocalDateTime lastTime;
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 直属上级
+     */
+    private Long parentId;
+    /**
+     * 姓名
+     */
+    private String name;
+    /**
+     * 职位
+     */
+    private String post;
+    /**
+     * QQ
+     */
+    private Long qq;
+    /**
+     * 微信
+     */
+    private String webchat;
+    /**
+     * 性别
+     */
+    private Integer sex;
+    /**
+     * 是否关键决策人
+     */
+    private Boolean policyMakers;
+    /**
+     * 负责人用户编号
+     */
+    private String ownerUserId;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
index 7643e4d4c..ef336c37e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/ContactMapper.java
@@ -1,14 +1,13 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.contact;
 
+import java.util.*;
+
 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.contact.vo.ContactExportReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactPageReqVO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import org.apache.ibatis.annotations.Mapper;
-
-import java.util.List;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 
 /**
  * crm联系人 Mapper
@@ -20,33 +19,45 @@ public interface ContactMapper extends BaseMapperX<ContactDO> {
 
     default PageResult<ContactDO> selectPage(ContactPageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<ContactDO>()
-                .likeIfPresent(ContactDO::getName, reqVO.getName())
                 .betweenIfPresent(ContactDO::getNextTime, reqVO.getNextTime())
                 .eqIfPresent(ContactDO::getMobile, reqVO.getMobile())
                 .eqIfPresent(ContactDO::getTelephone, reqVO.getTelephone())
                 .eqIfPresent(ContactDO::getEmail, reqVO.getEmail())
-                .eqIfPresent(ContactDO::getPost, reqVO.getPost())
                 .eqIfPresent(ContactDO::getCustomerId, reqVO.getCustomerId())
                 .eqIfPresent(ContactDO::getAddress, reqVO.getAddress())
                 .eqIfPresent(ContactDO::getRemark, reqVO.getRemark())
                 .betweenIfPresent(ContactDO::getCreateTime, reqVO.getCreateTime())
                 .betweenIfPresent(ContactDO::getLastTime, reqVO.getLastTime())
+                .eqIfPresent(ContactDO::getParentId, reqVO.getParentId())
+                .likeIfPresent(ContactDO::getName, reqVO.getName())
+                .eqIfPresent(ContactDO::getPost, reqVO.getPost())
+                .eqIfPresent(ContactDO::getQq, reqVO.getQq())
+                .eqIfPresent(ContactDO::getWebchat, reqVO.getWebchat())
+                .eqIfPresent(ContactDO::getSex, reqVO.getSex())
+                .eqIfPresent(ContactDO::getPolicyMakers, reqVO.getPolicyMakers())
+                .eqIfPresent(ContactDO::getOwnerUserId, reqVO.getOwnerUserId())
                 .orderByDesc(ContactDO::getId));
     }
 
     default List<ContactDO> selectList(ContactExportReqVO reqVO) {
         return selectList(new LambdaQueryWrapperX<ContactDO>()
-                .likeIfPresent(ContactDO::getName, reqVO.getName())
                 .betweenIfPresent(ContactDO::getNextTime, reqVO.getNextTime())
                 .eqIfPresent(ContactDO::getMobile, reqVO.getMobile())
                 .eqIfPresent(ContactDO::getTelephone, reqVO.getTelephone())
                 .eqIfPresent(ContactDO::getEmail, reqVO.getEmail())
-                .eqIfPresent(ContactDO::getPost, reqVO.getPost())
                 .eqIfPresent(ContactDO::getCustomerId, reqVO.getCustomerId())
                 .eqIfPresent(ContactDO::getAddress, reqVO.getAddress())
                 .eqIfPresent(ContactDO::getRemark, reqVO.getRemark())
                 .betweenIfPresent(ContactDO::getCreateTime, reqVO.getCreateTime())
                 .betweenIfPresent(ContactDO::getLastTime, reqVO.getLastTime())
+                .eqIfPresent(ContactDO::getParentId, reqVO.getParentId())
+                .likeIfPresent(ContactDO::getName, reqVO.getName())
+                .eqIfPresent(ContactDO::getPost, reqVO.getPost())
+                .eqIfPresent(ContactDO::getQq, reqVO.getQq())
+                .eqIfPresent(ContactDO::getWebchat, reqVO.getWebchat())
+                .eqIfPresent(ContactDO::getSex, reqVO.getSex())
+                .eqIfPresent(ContactDO::getPolicyMakers, reqVO.getPolicyMakers())
+                .eqIfPresent(ContactDO::getOwnerUserId, reqVO.getOwnerUserId())
                 .orderByDesc(ContactDO::getId));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
index ecb6fd577..4e835f0e7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
@@ -1,12 +1,10 @@
 package cn.iocoder.yudao.module.crm.service.contact;
 
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import java.util.*;
+import javax.validation.*;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-
-import javax.validation.Valid;
-import java.util.Collection;
-import java.util.List;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 
 /**
  * crm联系人 Service 接口
@@ -19,10 +17,9 @@ public interface ContactService {
      * 创建crm联系人
      *
      * @param createReqVO 创建信息
-     * @param userId      用户编号
      * @return 编号
      */
-    Long createContact(@Valid ContactCreateReqVO createReqVO, Long userId);
+    Long createContact(@Valid ContactCreateReqVO createReqVO);
 
     /**
      * 更新crm联系人
@@ -71,11 +68,8 @@ public interface ContactService {
     List<ContactDO> getContactList(ContactExportReqVO exportReqVO);
 
     /**
-     * 联系人编号
-     *
-     * @param reqVO  请求
-     * @param userId 用户编号
+     * 获取所有联系人列表,只返回姓名和id
+     * @return 所有联系人列表
      */
-    void transferContact(CrmContactTransferReqVO reqVO, Long userId);
-
+    List<ContactDO> allContactList();
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index dafebb598..fcd067e88 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -1,27 +1,22 @@
 package cn.iocoder.yudao.module.crm.service.contact;
 
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
+
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
-import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
-import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
-import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
-import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.validation.annotation.Validated;
-
-import javax.annotation.Resource;
-import java.util.Collection;
-import java.util.List;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTACT_NOT_EXISTS;
 
 /**
  * crm联系人 Service 实现类
@@ -35,41 +30,25 @@ public class ContactServiceImpl implements ContactService {
     @Resource
     private ContactMapper contactMapper;
 
-    @Resource
-    private CrmPermissionService crmPermissionService;
-
     @Override
-    public Long createContact(ContactCreateReqVO createReqVO, Long userId) {
-        // TODO @customerId:需要校验存在
+    public Long createContact(ContactCreateReqVO createReqVO) {
         // 插入
         ContactDO contact = ContactConvert.INSTANCE.convert(createReqVO);
         contactMapper.insert(contact);
-
-        // 创建数据权限
-        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CONTACTS.getType())
-                .setBizId(contact.getId()).setUserId(userId).setPermissionLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
-
         // 返回
         return contact.getId();
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, getIdFor = ContactUpdateReqVO.class,
-            permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void updateContact(ContactUpdateReqVO updateReqVO) {
         // 校验存在
         validateContactExists(updateReqVO.getId());
-        // TODO @customerId:需要校验存在
-
         // 更新
         ContactDO updateObj = ContactConvert.INSTANCE.convert(updateReqVO);
         contactMapper.updateById(updateObj);
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, permissionLevel = CrmPermissionLevelEnum.WRITE)
     public void deleteContact(Long id) {
         // 校验存在
         validateContactExists(id);
@@ -77,16 +56,13 @@ public class ContactServiceImpl implements ContactService {
         contactMapper.deleteById(id);
     }
 
-    private ContactDO validateContactExists(Long id) {
-        ContactDO contact = contactMapper.selectById(id);
-        if (contact == null) {
+    private void validateContactExists(Long id) {
+        if (contactMapper.selectById(id) == null) {
             throw exception(CONTACT_NOT_EXISTS);
         }
-        return contact;
     }
 
     @Override
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, permissionLevel = CrmPermissionLevelEnum.READ)
     public ContactDO getContact(Long id) {
         return contactMapper.selectById(id);
     }
@@ -110,15 +86,8 @@ public class ContactServiceImpl implements ContactService {
     }
 
     @Override
-    public void transferContact(CrmContactTransferReqVO reqVO, Long userId) {
-        // 1 校验联系人是否存在
-        validateContactExists(reqVO.getId());
-
-        // 2. 数据权限转移
-        crmPermissionService.transferPermission(
-                ContactConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CONTACTS.getType()));
-
-        // 3. TODO 记录转移日志
+    public List<ContactDO> allContactList() {
+        return contactMapper.selectList();
     }
 
 }

From 7af80ba2487981ec9a9d8db15109c3f6e047318c Mon Sep 17 00:00:00 2001
From: zyna <chenjidemenglin20@126.com>
Date: Sat, 11 Nov 2023 23:32:42 +0800
Subject: [PATCH 088/101] =?UTF-8?q?crm=E8=81=94=E7=B3=BB=E4=BA=BA=E8=A1=A8?=
 =?UTF-8?q?=E7=BB=93=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index e3e2ca0a0..f1442a2c7 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -1 +1,28 @@
 SET NAMES utf8mb4;
+
+CREATE TABLE `crm_contact` (
+                               `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
+                               `name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系人名称',
+                               `next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
+                               `mobile` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
+                               `telephone` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
+                               `email` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电子邮箱',
+                               `post` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '职务',
+                               `customer_id` bigint(20) DEFAULT NULL COMMENT '客户编号',
+                               `address` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
+                               `remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+                               `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人',
+                               `owner_user_id` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '负责人用户编号',
+                               `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
+                               `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
+                               `last_time` timestamp NULL DEFAULT NULL COMMENT '最后跟进时间',
+                               `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人',
+                               `deleted` bit(1) NOT NULL DEFAULT b'0',
+                               `tenant_id` bigint(20) DEFAULT NULL,
+                               `parent_id` bigint(20) DEFAULT NULL COMMENT '直系上属',
+                               `qq` int(11) DEFAULT NULL,
+                               `webchat` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+                               `sex` int(1) DEFAULT NULL COMMENT '性别',
+                               `policy_makers` bit(1) DEFAULT NULL COMMENT '是否关键决策人',
+                               PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUAUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='crm联系人';
\ No newline at end of file

From 322411304ce6587c8fc69429a98fe4449fce11e2 Mon Sep 17 00:00:00 2001
From: Wanwan <913752709@qq.com>
Date: Sun, 12 Nov 2023 14:34:49 +0800
Subject: [PATCH 089/101] =?UTF-8?q?feat:=20CRM=20=E5=AE=A2=E6=88=B7?=
 =?UTF-8?q?=E9=99=90=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../CrmCustomerLimitConfigServiceImpl.java    | 21 +++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
index 101aa63e0..2a6a3ca5d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
@@ -7,11 +7,15 @@ import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimit
 import cn.iocoder.yudao.module.crm.convert.customerlimitconfig.CrmCustomerLimitConfigConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customerlimitconfig.CrmCustomerLimitConfigMapper;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 
+import java.util.Collection;
+
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_LIMIT_CONFIG_NOT_EXISTS;
 
@@ -26,9 +30,14 @@ public class CrmCustomerLimitConfigServiceImpl implements CrmCustomerLimitConfig
 
     @Resource
     private CrmCustomerLimitConfigMapper customerLimitConfigMapper;
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private AdminUserApi adminUserApi;
 
     @Override
     public Long createCustomerLimitConfig(CrmCustomerLimitConfigCreateReqVO createReqVO) {
+        validateUserAndDept(createReqVO.getUserIds(), createReqVO.getDeptIds());
         // 插入
         CrmCustomerLimitConfigDO customerLimitConfig = CrmCustomerLimitConfigConvert.INSTANCE.convert(createReqVO);
         customerLimitConfigMapper.insert(customerLimitConfig);
@@ -40,6 +49,7 @@ public class CrmCustomerLimitConfigServiceImpl implements CrmCustomerLimitConfig
     public void updateCustomerLimitConfig(CrmCustomerLimitConfigUpdateReqVO updateReqVO) {
         // 校验存在
         validateCustomerLimitConfigExists(updateReqVO.getId());
+        validateUserAndDept(updateReqVO.getUserIds(), updateReqVO.getDeptIds());
         // 更新
         CrmCustomerLimitConfigDO updateObj = CrmCustomerLimitConfigConvert.INSTANCE.convert(updateReqVO);
         customerLimitConfigMapper.updateById(updateObj);
@@ -69,4 +79,15 @@ public class CrmCustomerLimitConfigServiceImpl implements CrmCustomerLimitConfig
         }
     }
 
+    /**
+     * 校验入参的用户和部门
+     *
+     * @param userIds 用户 ids
+     * @param deptIds 部门 ids
+     */
+    private void validateUserAndDept(Collection<Long> userIds, Collection<Long> deptIds) {
+        deptApi.validateDeptList(deptIds);
+        adminUserApi.validateUserList(userIds);
+    }
+
 }

From 794649ea8a55741e43c1e2ac15a687607c7360e9 Mon Sep 17 00:00:00 2001
From: puhui999 <puhui999@163.com>
Date: Tue, 14 Nov 2023 11:23:52 +0800
Subject: [PATCH 090/101] =?UTF-8?q?CRM-=E6=95=B0=E6=8D=AE=E6=9D=83?=
 =?UTF-8?q?=E9=99=90=EF=BC=9A=E5=AE=8C=E5=96=84=E5=9B=A2=E9=98=9F=E6=88=90?=
 =?UTF-8?q?=E5=91=98=E7=9B=B8=E5=85=B3=E6=93=8D=E4=BD=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../permission/CrmPermissionController.java   | 39 +++++++++++++------
 .../permission/vo/CrmPermissionRespVO.java    |  4 ++
 .../vo/CrmPermissionUpdateReqVO.java          | 30 +++++++++-----
 .../permission/CrmPermissionConvert.java      |  9 +++++
 .../mysql/permission/CrmPermissionMapper.java |  5 +++
 .../permission/CrmPermissionService.java      | 19 ++++++---
 .../permission/CrmPermissionServiceImpl.java  | 35 ++++++++++-------
 7 files changed, 102 insertions(+), 39 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
index ab9d7907a..a13694e1a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
@@ -28,17 +28,16 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_NOT_EXISTS;
 
 @Tag(name = "管理后台 - CRM 数据权限(数据团队成员操作)")
 @RestController
@@ -85,12 +84,12 @@ public class CrmPermissionController {
     }
 
     @PutMapping("/update")
-    @Operation(summary = "编辑团队成员")
+    @Operation(summary = "编辑团队成员权限")
     @PreAuthorize("@ss.hasPermission('crm:permission:update')")
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#updateReqVO.bizType", bizId = "#updateReqVO.bizId"
-            , level = CrmPermissionLevelEnum.WRITE)
+            , level = CrmPermissionLevelEnum.OWNER)
     public CommonResult<Boolean> updatePermission(@Valid @RequestBody CrmPermissionUpdateReqVO updateReqVO) {
-        permissionService.updatePermission(CrmPermissionConvert.INSTANCE.convert(updateReqVO));
+        permissionService.updatePermission(updateReqVO);
         return success(true);
     }
 
@@ -99,15 +98,33 @@ public class CrmPermissionController {
     @Parameters({
             @Parameter(name = "bizType", description = "CRM 类型", required = true, example = "2"),
             @Parameter(name = "bizId", description = "CRM 类型数据编号", required = true, example = "1024"),
-            @Parameter(name = "id", description = "团队成员编号", required = true, example = "1024")
+            @Parameter(name = "ids", description = "团队成员编号", required = true, example = "1024")
     })
     @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#bizType", bizId = "#bizId"
-            , level = CrmPermissionLevelEnum.OWNER)
+            , level = CrmPermissionLevelEnum.OWNER) // 为了校验权限请求必须带上 bizType 和  bizId
     public CommonResult<Boolean> deletePermission(@RequestParam("bizType") Integer bizType,
                                                   @RequestParam("bizId") Long bizId,
-                                                  @RequestParam("id") Long id) {
-        permissionService.deletePermission(id);
+                                                  @RequestParam("ids") Collection<Long> ids) {
+        permissionService.deletePermission(ids);
+        return success(true);
+    }
+
+    @DeleteMapping("/quit-team")
+    @Operation(summary = "退出团队")
+    @Parameters({
+            @Parameter(name = "id", description = "团队成员编号", required = true, example = "1024")
+    })
+    @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
+    public CommonResult<Boolean> deletePermission(@RequestParam("id") Long id) {
+        // 校验数据存在且是自己
+        CrmPermissionDO permission = permissionService.getPermissionByIdAndUserId(id, getLoginUserId());
+        if (permission == null) {
+            throw exception(CRM_PERMISSION_NOT_EXISTS);
+        }
+
+        // 删除
+        permissionService.deletePermission(Collections.singletonList(id));
         return success(true);
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
index a05dfd61a..acd7003a0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.permission.vo;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.time.LocalDateTime;
 import java.util.Set;
 
 @Schema(description = "管理后台 - CRM 数据权限(团队成员) Response VO")
@@ -21,4 +22,7 @@ public class CrmPermissionRespVO extends CrmPermissionBaseVO {
     @Schema(description = "岗位名称数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[BOOS,经理]")
     private Set<String> postNames;
 
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-01-01 00:00:00")
+    private LocalDateTime createTime;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
index 2c83e0751..a6f0b1133 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionUpdateReqVO.java
@@ -1,22 +1,34 @@
 package cn.iocoder.yudao.module.crm.controller.admin.permission.vo;
 
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
 
 import javax.validation.constraints.NotNull;
+import java.util.List;
 
 @Schema(description = "管理后台 - CRM 数据权限更新 Request VO")
 @Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmPermissionUpdateReqVO extends CrmPermissionBaseVO {
+public class CrmPermissionUpdateReqVO {
 
-    @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
-    @NotNull(message = "数据权限编号不能为空")
-    private Long id;
+    @Schema(description = "数据权限编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2]")
+    @NotNull(message = "数据权限编号列表不能为空")
+    private List<Long> ids;
 
-    // TODO @puhui999:是不是只更新 permission??? 是的
+    @Schema(description = "Crm 类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @InEnum(CrmBizTypeEnum.class)
+    @NotNull(message = "Crm 类型不能为空")
+    private Integer bizType;
+
+    @Schema(description = "Crm 类型数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "Crm 类型数据编号不能为空")
+    private Long bizId;
+
+    @Schema(description = "权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @InEnum(CrmPermissionLevelEnum.class)
+    @NotNull(message = "权限级别不能为空")
+    private Integer level;
 
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
index 147cdd01a..2b8e321d6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
@@ -15,6 +15,7 @@ import com.google.common.collect.Multimaps;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -54,4 +55,12 @@ public interface CrmPermissionConvert {
         });
     }
 
+    default List<CrmPermissionDO> convertList(CrmPermissionUpdateReqVO updateReqVO) {
+        List<CrmPermissionDO> permissions = new ArrayList<>();
+        updateReqVO.getIds().forEach(id -> {
+            permissions.add(new CrmPermissionDO().setId(id).setLevel(updateReqVO.getLevel()));
+        });
+        return permissions;
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
index 34a80c2c4..ad54a7604 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
@@ -42,4 +42,9 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 .eq(CrmPermissionDO::getLevel, level));
     }
 
+    default CrmPermissionDO selectByIdAndUserId(Long id, Long userId) {
+        return selectOne(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getId, id).eq(CrmPermissionDO::getUserId, userId));
+    }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
index 8fbfafbe2..96ac1e145 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -1,11 +1,11 @@
 package cn.iocoder.yudao.module.crm.service.permission;
 
 
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 
 import javax.validation.Valid;
 import java.util.Collection;
@@ -29,16 +29,16 @@ public interface CrmPermissionService {
     /**
      * 更新数据权限
      *
-     * @param updateBO 更新信息
+     * @param updateReqVO 更新信息
      */
-    void updatePermission(@Valid CrmPermissionUpdateReqBO updateBO);
+    void updatePermission(CrmPermissionUpdateReqVO updateReqVO);
 
     /**
      * 删除数据权限
      *
-     * @param id 编号
+     * @param ids 编号
      */
-    void deletePermission(Long id);
+    void deletePermission(Collection<Long> ids);
 
     /**
      * 获取用户数据权限通过 数据类型 x 某个数据 x 用户编号
@@ -50,6 +50,15 @@ public interface CrmPermissionService {
      */
     CrmPermissionDO getPermissionByBizTypeAndBizIdAndUserId(Integer bizType, Long bizId, Long userId);
 
+    /**
+     * 获取用户数据权限通过 权限编号 x 用户编号
+     *
+     * @param id     权限编号
+     * @param userId 用户编号
+     * @return 数据权限
+     */
+    CrmPermissionDO getPermissionByIdAndUserId(Long id, Long userId);
+
     /**
      * 获取数据权限列表,通过 数据类型 x 某个数据
      *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index a87608e1f..e7efd1284 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.service.permission;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
@@ -9,7 +10,6 @@ import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
-import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionUpdateReqBO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -53,25 +53,22 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void updatePermission(CrmPermissionUpdateReqBO updateBO) {
-        // 1.1 校验用户是否存在
-        adminUserApi.validateUserList(Collections.singletonList(updateBO.getUserId()));
-        // 1.2 校验存在
-        validateCrmPermissionExists(updateBO.getId());
+    public void updatePermission(CrmPermissionUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateCrmPermissionExists(updateReqVO.getIds());
 
-        // 2. 更新操作
-        CrmPermissionDO updateDO = CrmPermissionConvert.INSTANCE.convert(updateBO);
-        crmPermissionMapper.updateById(updateDO);
+        List<CrmPermissionDO> updateDO = CrmPermissionConvert.INSTANCE.convertList(updateReqVO);
+        crmPermissionMapper.updateBatch(updateDO);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void deletePermission(Long id) {
+    public void deletePermission(Collection<Long> ids) {
         // 校验存在
-        validateCrmPermissionExists(id);
+        validateCrmPermissionExists(ids);
 
         // 删除
-        crmPermissionMapper.deleteById(id);
+        crmPermissionMapper.deleteBatchIds(ids);
     }
 
     @Override
@@ -79,6 +76,11 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         return crmPermissionMapper.selectByBizTypeAndBizIdByUserId(bizType, bizId, userId);
     }
 
+    @Override
+    public CrmPermissionDO getPermissionByIdAndUserId(Long id, Long userId) {
+        return crmPermissionMapper.selectByIdAndUserId(id, userId);
+    }
+
     @Override
     public List<CrmPermissionDO> getPermissionByBizTypeAndBizId(Integer bizType, Long bizId) {
         return crmPermissionMapper.selectByBizTypeAndBizId(bizType, bizId);
@@ -89,13 +91,16 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         return crmPermissionMapper.selectListByBizTypeAndBizIdsAndLevel(bizType, bizIds, level);
     }
 
-    private void validateCrmPermissionExists(Long id) {
-        if (crmPermissionMapper.selectById(id) == null) {
+    private void validateCrmPermissionExists(Collection<Long> ids) {
+        List<CrmPermissionDO> permissionList = crmPermissionMapper.selectBatchIds(ids);
+        // 校验存在
+        if (ObjUtil.notEqual(permissionList.size(), ids.size())) {
             throw exception(CRM_PERMISSION_NOT_EXISTS);
         }
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void transferPermission(CrmPermissionTransferReqBO transferReqBO) {
         // 1. 校验数据权限-是否是负责人,只有负责人才可以转移
         CrmPermissionDO oldPermission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(transferReqBO.getBizType(),
@@ -142,6 +147,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void receiveBiz(Integer bizType, Long bizId, Long userId) {
         CrmPermissionDO permission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(bizType, bizId, CrmPermissionDO.POOL_USER_ID);
         if (permission == null) { // 不存在则模块数据也不存在
@@ -152,6 +158,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void putPool(Integer bizType, Long bizId, Long userId) {
         CrmPermissionDO permission = crmPermissionMapper.selectByBizTypeAndBizIdByUserId(bizType, bizId, userId);
         if (permission == null) { // 不存在则模块数据也不存在

From 330380c2d0dd58af81af921e6b781d95395721c4 Mon Sep 17 00:00:00 2001
From: owen <owen@evolsun.com>
Date: Fri, 17 Nov 2023 09:56:52 +0800
Subject: [PATCH 091/101] =?UTF-8?q?=E8=90=A5=E9=94=80=EF=BC=9A=E9=80=82?=
 =?UTF-8?q?=E9=85=8D=E5=95=86=E5=9F=8E=E8=A3=85=E4=BF=AE=E7=BB=84=E4=BB=B6?=
 =?UTF-8?q?=E3=80=90=E5=95=86=E5=93=81=E5=8D=A1=E7=89=87=E3=80=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/spu/vo/ProductSpuDetailRespVO.java  | 14 +------------
 .../admin/spu/vo/ProductSpuRespVO.java        |  2 +-
 .../app/spu/AppProductSpuController.java      | 20 +++++++++++++++++++
 .../app/spu/vo/AppProductSpuPageRespVO.java   |  3 +++
 4 files changed, 25 insertions(+), 14 deletions(-)

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
index 1be96632d..336d44467 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
@@ -12,19 +12,7 @@ import java.util.List;
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ProductSpuDetailRespVO extends ProductSpuBaseVO {
-
-    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1212")
-    private Long id;
-
-    @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
-    private Integer salesCount;
-
-    @Schema(description = "浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000")
-    private Integer browseCount;
-
-    @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    private Integer status;
+public class ProductSpuDetailRespVO extends ProductSpuRespVO {
 
     // ========== SKU 相关字段 =========
 
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
index 0148cb2a1..faf8a5572 100755
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
 @ToString(callSuper = true)
 public class ProductSpuRespVO extends ProductSpuBaseVO {
 
-    @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
+    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
     private Long id;
 
     @Schema(description = "商品价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999")
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
index 8f49e7f74..9d0a1fe9f 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
@@ -30,6 +30,7 @@ import javax.annotation.Resource;
 import javax.validation.Valid;
 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.pojo.CommonResult.success;
@@ -75,6 +76,25 @@ public class AppProductSpuController {
         return success(voList);
     }
 
+    @GetMapping("/list-by-ids")
+    @Operation(summary = "获得商品 SPU 列表")
+    @Parameters({
+            @Parameter(name = "ids", description = "编号列表", required = true)
+    })
+    public CommonResult<List<AppProductSpuPageRespVO>> getSpuList(@RequestParam("ids") Set<Long> ids) {
+        List<ProductSpuDO> list = productSpuService.getSpuList(ids);
+        if (CollUtil.isEmpty(list)) {
+            return success(Collections.emptyList());
+        }
+
+        // 拼接返回
+        List<AppProductSpuPageRespVO> voList = ProductSpuConvert.INSTANCE.convertListForGetSpuList(list);
+        // 处理 vip 价格
+        MemberLevelRespDTO memberLevel = getMemberLevel();
+        voList.forEach(vo -> vo.setVipPrice(calculateVipPrice(vo.getPrice(), memberLevel)));
+        return success(voList);
+    }
+
     @GetMapping("/page")
     @Operation(summary = "获得商品 SPU 分页")
     public CommonResult<PageResult<AppProductSpuPageRespVO>> getSpuPage(@Valid AppProductSpuPageReqVO pageVO) {
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java
index c4a66afd2..07b0d8e95 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java
@@ -15,6 +15,9 @@ public class AppProductSpuPageRespVO {
     @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
     private String name;
 
+    @Schema(description = "商品简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖简介")
+    private String introduction;
+
     @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED)
     private Long categoryId;
 

From 966690125bfcbdb8b99de5d5b2150e94d2ad82e1 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 18 Nov 2023 20:43:32 +0800
Subject: [PATCH 092/101] =?UTF-8?q?crm=EF=BC=9Acode=20review=20=E8=81=94?=
 =?UTF-8?q?=E7=B3=BB=E4=BA=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm.sql                             | 28 -------
 .../admin/contact/ContactController.java      | 81 ++++++++++---------
 .../admin/contact/vo/ContactBaseVO.java       | 17 ++--
 .../admin/contact/vo/ContactCreateReqVO.java  |  8 +-
 .../admin/contact/vo/ContactExcelVO.java      | 14 ++--
 .../admin/contact/vo/ContactExportReqVO.java  | 11 ++-
 .../admin/contact/vo/ContactPageReqVO.java    | 15 ++--
 .../admin/contact/vo/ContactRespVO.java       | 19 +++--
 .../admin/contact/vo/ContactSimpleRespVO.java |  7 +-
 .../admin/contact/vo/ContactUpdateReqVO.java  | 10 ++-
 .../crm/dal/dataobject/contact/ContactDO.java | 19 +++--
 .../service/contact/ContactServiceImpl.java   | 34 ++++----
 12 files changed, 123 insertions(+), 140 deletions(-)

diff --git a/sql/mysql/crm.sql b/sql/mysql/crm.sql
index f1442a2c7..e69de29bb 100644
--- a/sql/mysql/crm.sql
+++ b/sql/mysql/crm.sql
@@ -1,28 +0,0 @@
-SET NAMES utf8mb4;
-
-CREATE TABLE `crm_contact` (
-                               `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
-                               `name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系人名称',
-                               `next_time` datetime DEFAULT NULL COMMENT '下次联系时间',
-                               `mobile` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
-                               `telephone` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
-                               `email` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电子邮箱',
-                               `post` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '职务',
-                               `customer_id` bigint(20) DEFAULT NULL COMMENT '客户编号',
-                               `address` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
-                               `remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
-                               `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人',
-                               `owner_user_id` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '负责人用户编号',
-                               `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
-                               `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
-                               `last_time` timestamp NULL DEFAULT NULL COMMENT '最后跟进时间',
-                               `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人',
-                               `deleted` bit(1) NOT NULL DEFAULT b'0',
-                               `tenant_id` bigint(20) DEFAULT NULL,
-                               `parent_id` bigint(20) DEFAULT NULL COMMENT '直系上属',
-                               `qq` int(11) DEFAULT NULL,
-                               `webchat` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
-                               `sex` int(1) DEFAULT NULL COMMENT '性别',
-                               `policy_makers` bit(1) DEFAULT NULL COMMENT '是否关键决策人',
-                               PRIMARY KEY (`id`) USING BTREE
-) ENGINE=InnoDB AUAUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='crm联系人';
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
index d1e0c5ba5..3a5406419 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -2,42 +2,41 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.NumberUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.contact.vo.*;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerExportReqVO;
+import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.service.contact.ContactService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import com.google.common.collect.Lists;
-import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.security.access.prepost.PreAuthorize;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
 
-import javax.validation.constraints.*;
-import javax.validation.*;
-import javax.servlet.http.*;
-import java.util.*;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
 import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 
-import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-
-import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
-
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
-import cn.iocoder.yudao.module.crm.service.contact.ContactService;
-
-@Tag(name = "管理后台 - crm联系人")
+// TODO @zya:crm 所有的类,dou带 Crm 前缀,因为它的名字太通用了,可能和后续的 erp 之类的冲突
+@Tag(name = "管理后台 - CRM 联系人")
 @RestController
 @RequestMapping("/crm/contact")
 @Validated
@@ -45,20 +44,22 @@ public class ContactController {
 
     @Resource
     private ContactService contactService;
-    @Resource
-    private AdminUserApi adminUserApi;
+    // TODO @zyna:模块内,注入的变量,不用带 crm 前缀哈
     @Resource
     private CrmCustomerService crmCustomerService;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @PostMapping("/create")
-    @Operation(summary = "创建crm联系人")
+    @Operation(summary = "创建联系人")
     @PreAuthorize("@ss.hasPermission('crm:contact:create')")
     public CommonResult<Long> createContact(@Valid @RequestBody ContactCreateReqVO createReqVO) {
         return success(contactService.createContact(createReqVO));
     }
 
     @PutMapping("/update")
-    @Operation(summary = "更新crm联系人")
+    @Operation(summary = "更新联系人")
     @PreAuthorize("@ss.hasPermission('crm:contact:update')")
     public CommonResult<Boolean> updateContact(@Valid @RequestBody ContactUpdateReqVO updateReqVO) {
         contactService.updateContact(updateReqVO);
@@ -66,7 +67,7 @@ public class ContactController {
     }
 
     @DeleteMapping("/delete")
-    @Operation(summary = "删除crm联系人")
+    @Operation(summary = "删除联系人")
     @Parameter(name = "id", description = "编号", required = true)
     @PreAuthorize("@ss.hasPermission('crm:contact:delete')")
     public CommonResult<Boolean> deleteContact(@RequestParam("id") Long id) {
@@ -75,12 +76,14 @@ public class ContactController {
     }
 
     @GetMapping("/get")
-    @Operation(summary = "获得crm联系人")
+    @Operation(summary = "获得联系人")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<ContactRespVO> getContact(@RequestParam("id") Long id) {
         ContactDO contact = contactService.getContact(id);
+        // TODO @zyna:需要考虑 null 的情况;
         ContactRespVO contactRespVO  = ContactConvert.INSTANCE.convert(contact);
+        // TODO @zyna:可以把数据读完后,convert 统一交给 ContactConvert,让 controller 更简洁;而 convert 专门去做一些转换逻辑
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(CollUtil.removeNull(Lists.newArrayList(
                 NumberUtil.parseLong(contact.getCreator()))));
         contactRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(contact.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
@@ -88,27 +91,24 @@ public class ContactController {
         return success(contactRespVO);
     }
 
-    @GetMapping("/list")
-    @Operation(summary = "获得crm联系人列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
-    public CommonResult<List<ContactRespVO>> getContactList(@RequestParam("ids") Collection<Long> ids) {
-        List<ContactDO> list = contactService.getContactList(ids);
-        return success(ContactConvert.INSTANCE.convertList(list));
-    }
+    // TODO @zyna:url 使用中划线噢;然后,单词的拼写也要注意呀,AllList 是不是更好呀;
     @GetMapping("/simpleAlllist")
-    @Operation(summary = "获得crm联系人列表")
+    @Operation(summary = "获得联系人列表")
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<List<ContactSimpleRespVO>> simpleAlllist() {
+        // TODO @zyna:方法名改成,getContactList;方法命名,要动名词,get 动词;all 可以去掉,因为没条件,自然是全部
         List<ContactDO> list = contactService.allContactList();
         return success(ContactConvert.INSTANCE.convertAllList(list));
     }
+
     @GetMapping("/page")
-    @Operation(summary = "获得crm联系人分页")
+    @Operation(summary = "获得联系人分页")
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<PageResult<ContactRespVO>> getContactPage(@Valid ContactPageReqVO pageVO) {
         PageResult<ContactDO> pageData = contactService.getContactPage(pageVO);
         PageResult<ContactRespVO> pageResult =ContactConvert.INSTANCE.convertPage(pageData);
+        // TODO @zyna:需要考虑 null 的情况;
+        // TODO @zyna:可以把数据读完后,convert 统一交给 ContactConvert,让 controller 更简洁;而 convert 专门去做一些转换逻辑
         //待接口实现后修改
         List<CrmCustomerDO> crmCustomerDOList = crmCustomerService.getCustomerList(new CrmCustomerExportReqVO());
         Map<Long,CrmCustomerDO> crmCustomerDOMap = crmCustomerDOList.stream().collect(Collectors.toMap(CrmCustomerDO::getId,v->v));
@@ -118,8 +118,9 @@ public class ContactController {
         return success(pageResult);
     }
 
+    // TODO @zyna:可以看下新的导出写法,这里调整下
     @GetMapping("/export-excel")
-    @Operation(summary = "导出crm联系人 Excel")
+    @Operation(summary = "导出联系人 Excel")
     @PreAuthorize("@ss.hasPermission('crm:contact:export')")
     @OperateLog(type = EXPORT)
     public void exportContactExcel(@Valid ContactExportReqVO exportReqVO,
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
index 6a75dbf2e..9311ad365 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
@@ -1,26 +1,25 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-
-import java.time.LocalDate;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import javax.validation.constraints.*;
+import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
+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;
 
+// TODO zyna:参考新的 vo,重新拆分下 VO
 /**
- * crm联系人 Base VO,提供给添加、修改、详细的子 VO 使用
+ * CRM 联系人 Base VO,提供给添加、修改、详细的子 VO 使用
  * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
  */
 @Data
 public class ContactBaseVO {
 
+    // TODO @zyna:example 最好都写下
+    // TODO @zyna:必要的字段校验,例如说 @Mobile,@Emal 等等
+
     @Schema(description = "下次联系时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
     private LocalDateTime nextTime;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
index 424d945dc..5eccfea74 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
@@ -1,11 +1,11 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
-import lombok.*;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
 
-@Schema(description = "管理后台 - crm联系人创建 Request VO")
+@Schema(description = "管理后台 - CRM 联系人创建 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
index f7ca2b8d0..d13db6c3e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
@@ -1,22 +1,18 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-
-import java.time.LocalDate;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-
 import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
 
+import java.time.LocalDateTime;
+
+// TODO @zyna:参考新的 VO 结构,把 ContactExcelVO 融合到 ContactRespVO 中
 /**
  * crm联系人 Excel VO
  *
  * @author 芋道源码
  */
 @Data
+@Deprecated
 public class ContactExcelVO {
 
     @ExcelProperty("下次联系时间")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
index 180ddb6bd..f05f6dcde 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
@@ -1,18 +1,17 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
-import lombok.*;
-
-import java.time.LocalDate;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import java.time.LocalDateTime;
+import lombok.Data;
 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;
 
+// TODO @zyna:参考新的 VO 结构,使用 ContactPageReqVO 查询导出的数据
 @Schema(description = "管理后台 - crm联系人 Excel 导出 Request VO,参数和 ContactPageReqVO 是一致的")
 @Data
+@Deprecated
 public class ContactExportReqVO {
 
     @Schema(description = "下次联系时间")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
index 7aac9083a..a20826b0a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
@@ -1,12 +1,12 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
-import lombok.*;
-
-import java.time.LocalDate;
-import java.util.*;
-import io.swagger.v3.oas.annotations.media.Schema;
 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;
@@ -17,6 +17,11 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class ContactPageReqVO extends PageParam {
 
+    // TODO @zyna:筛选条件
+    // ●客户:
+    // ●姓名:
+    // ●手机、电话、座机、QQ、微信、邮箱
+
     @Schema(description = "下次联系时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] nextTime;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
index 062d9eee5..5a69424dc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
@@ -4,19 +4,24 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import java.time.LocalDateTime;
 
-@Schema(description = "管理后台 - crm联系人 Response VO")
+@Schema(description = "管理后台 - CRM 联系人 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 public class ContactRespVO extends ContactBaseVO {
 
-    @Schema(description = "创建时间")
-    private LocalDateTime createTime;
-    @Schema(description = "创建人")
-    private String creatorName;
-    @Schema(description = "客户")
-    private String customerName;
     @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
     private Long id;
 
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    // TODO  @zyna:example 最好写下;
+
+    @Schema(description = "创建人")
+    private String creatorName;
+
+    @Schema(description = "客户名字")
+    private String customerName;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java
index dd37802d5..98d7da034 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java
@@ -2,16 +2,13 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
-import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-import java.time.LocalDateTime;
-
-@Schema(description = "管理后台 - crm联系人 Response VO")
+@Schema(description = "管理后台 - CRM 联系人 Response VO")
 @Data
 @ToString(callSuper = true)
 public class ContactSimpleRespVO {
-    @Schema(description = "姓名", example = "芋艿")
+    @Schema(description = "姓名", example = "芋艿") // TODO @zyna:requiredMode = Schema.RequiredMode.REQUIRED;需要空一行;字段的顺序改下,id 在 name 前面,会更干净
     private String name;
 
     @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
index 6aeaaec7c..809009b0e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
@@ -1,11 +1,13 @@
 package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
 
-@Schema(description = "管理后台 - crm联系人更新 Request VO")
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 联系人更新 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
index 513ffba45..f98da52bc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/ContactDO.java
@@ -1,17 +1,15 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
 
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 
-import java.time.LocalDate;
-import java.util.*;
 import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import com.baomidou.mybatisplus.annotation.*;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 
 /**
- * crm联系人 DO
+ * CRM 联系人 DO
  *
  * @author 芋道源码
  */
@@ -25,6 +23,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 @AllArgsConstructor
 public class ContactDO extends BaseDO {
 
+    // TODO @zyna:这个字段的顺序,是不是整理下;
     /**
      * 下次联系时间
      */
@@ -57,11 +56,13 @@ public class ContactDO extends BaseDO {
      * 最后跟进时间
      */
     private LocalDateTime lastTime;
+    // TODO @zyna:这个放在最前面吧
     /**
      * 主键
      */
     @TableId
     private Long id;
+    // TODO @zyna:直接上级,最好写下它关联的字段,例如说这个,应该关联 ContactDO 的 id 字段
     /**
      * 直属上级
      */
@@ -78,18 +79,22 @@ public class ContactDO extends BaseDO {
      * QQ
      */
     private Long qq;
+    // TODO @zyna:wechat
     /**
      * 微信
      */
     private String webchat;
+    // TODO @zyna:关联的枚举
     /**
      * 性别
      */
     private Integer sex;
+    // TODO @zyna:这个字段改成 master 哈;
     /**
      * 是否关键决策人
      */
     private Boolean policyMakers;
+    // TODO @zyna:应该是 Long
     /**
      * 负责人用户编号
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index fcd067e88..1c2c930c5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -1,22 +1,24 @@
 package cn.iocoder.yudao.module.crm.service.contact;
 
-import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-
-import java.util.*;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
-import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
-
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTACT_NOT_EXISTS;
 
 /**
  * crm联系人 Service 实现类
@@ -30,7 +32,7 @@ public class ContactServiceImpl implements ContactService {
     @Resource
     private ContactMapper contactMapper;
 
-    @Override
+    @Override // TODO @zyna:新增和修改时,关联字段要校验,例如说 直属上级,是不是真的存在;
     public Long createContact(ContactCreateReqVO createReqVO) {
         // 插入
         ContactDO contact = ContactConvert.INSTANCE.convert(createReqVO);

From 8c9c91f0ca4daed135a02a8fc0d19906449e1d35 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 18 Nov 2023 22:12:12 +0800
Subject: [PATCH 093/101] =?UTF-8?q?crm=EF=BC=9Acode=20review=20=E9=85=8D?=
 =?UTF-8?q?=E7=BD=AE=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/crm_data.sql                        |  3 ---
 .../admin/customer/CrmCustomerController.java | 19 +++++++++-----
 .../CrmCustomerLimitConfigController.java     | 26 +++++++++++--------
 .../CrmCustomerPoolConfigController.java      |  2 ++
 .../vo/CrmCustomerLimitConfigBaseVO.java      |  1 +
 .../vo/CrmCustomerPoolConfigBaseVO.java       |  2 ++
 .../vo/CrmCustomerPoolConfigRespVO.java       |  1 +
 .../convert/customer/CrmCustomerConvert.java  |  4 +--
 .../CrmCustomerLimitConfigConvert.java        |  4 ++-
 .../customer/CrmCustomerPoolConfigDO.java     |  1 +
 .../dal/dataobject/customer/package-info.java |  4 ---
 .../CrmCustomerLimitConfigDO.java             |  6 ++++-
 .../CrmCustomerPoolConfigService.java         |  1 +
 .../CrmCustomerPoolConfigServiceImpl.java     |  2 ++
 .../CrmCustomerLimitConfigServiceImpl.java    |  2 +-
 .../mapper/business/CrmBusinessMapper.xml     | 12 ---------
 .../CrmBusinessStatusTypeMapper.xml           | 12 ---------
 .../resources/mapper/clue/CrmClueMapper.xml   | 12 ---------
 .../mapper/contact/ContactMapper.xml          | 12 ---------
 .../mapper/customer/CrmCustomerMapper.xml     | 12 ---------
 .../customer/CrmCustomerPoolConfigMapper.xml  | 12 ---------
 .../CrmCustomerLimitConfigMapper.xml          | 12 ---------
 .../mapper/product/ProductMapper.xml          | 12 ---------
 .../productcategory/ProductCategoryMapper.xml | 12 ---------
 .../mapper/receivable/CrmReceivableMapper.xml | 12 ---------
 .../receivable/CrmReceivablePlanMapper.xml    | 12 ---------
 ...CrmCustomerLimitConfigServiceImplTest.java |  1 +
 .../yudao/module/system/api/dept/DeptApi.java |  3 +--
 .../controller/admin/user/UserController.java |  1 +
 29 files changed, 50 insertions(+), 165 deletions(-)
 rename yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/{customerlimitconfig => customer}/CrmCustomerLimitConfigConvert.java (95%)
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/package-info.java
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/business/CrmBusinessMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/businessstatustype/CrmBusinessStatusTypeMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/contact/ContactMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerPoolConfigMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customerlimitconfig/CrmCustomerLimitConfigMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/product/ProductMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/productcategory/ProductCategoryMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivableMapper.xml
 delete mode 100644 yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivablePlanMapper.xml

diff --git a/sql/mysql/crm_data.sql b/sql/mysql/crm_data.sql
index 7453f8d25..b5be1e691 100644
--- a/sql/mysql/crm_data.sql
+++ b/sql/mysql/crm_data.sql
@@ -18,6 +18,3 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1399, 6, '支付宝', '6', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:38', '1', '2023-10-18 21:55:38', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1400, 7, '微信支付', '7', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:53', '1', '2023-10-18 21:55:53', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1401, 8, '其他', '8', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:56:06', '1', '2023-10-18 21:56:06', b'0');
-
-
-INSERT INTO `crm_customer_pool_config` (`id`, `enabled`, `contact_expire_days`, `deal_expire_days`, `notify_enabled`, `notify_days`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 1, 15, 30, 1, 3, '1', NOW(), '1', NOW(), b'0', 1);
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 3c7dbeb7a..73f7fdb7d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -5,8 +5,6 @@ import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
@@ -35,6 +33,8 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -46,6 +46,7 @@ public class CrmCustomerController {
 
     @Resource
     private CrmCustomerService customerService;
+
     @Resource
     private DeptApi deptApi;
     @Resource
@@ -103,11 +104,15 @@ public class CrmCustomerController {
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
         PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO);
-        Set<Long> userSet = CollectionUtils.convertSetByFlatMap(pageResult.getList(), i -> Stream.of(NumberUtil.parseLong(i.getCreator()), i.getOwnerUserId()));
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userSet);
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(userMap.values().stream().map(AdminUserRespDTO::getDeptId).collect(Collectors.toSet()));
-        PageResult<CrmCustomerRespVO> pageVo = CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap);
-        return success(pageVo);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty(pageResult.getTotal()));
+        }
+        // 拼接数据
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSetByFlatMap(pageResult.getList(), user -> Stream.of(NumberUtil.parseLong(user.getCreator()), user.getOwnerUserId())));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+                convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap));
     }
 
     @GetMapping("/export-excel")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
index 80f6e461b..b4d4751fc 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
@@ -1,13 +1,13 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigUpdateReqVO;
-import cn.iocoder.yudao.module.crm.convert.customerlimitconfig.CrmCustomerLimitConfigConvert;
+import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerLimitConfigConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
 import cn.iocoder.yudao.module.crm.service.customerlimitconfig.CrmCustomerLimitConfigService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -24,12 +24,10 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.validation.Valid;
 import java.util.Collection;
-import java.util.HashSet;
 import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
 
 @Tag(name = "管理后台 - 客户限制配置")
 @RestController
@@ -39,6 +37,7 @@ public class CrmCustomerLimitConfigController {
 
     @Resource
     private CrmCustomerLimitConfigService customerLimitConfigService;
+
     @Resource
     private DeptApi deptApi;
     @Resource
@@ -74,8 +73,9 @@ public class CrmCustomerLimitConfigController {
     @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:query')")
     public CommonResult<CrmCustomerLimitConfigRespVO> getCustomerLimitConfig(@RequestParam("id") Long id) {
         CrmCustomerLimitConfigDO customerLimitConfig = customerLimitConfigService.getCustomerLimitConfig(id);
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(new HashSet<>(customerLimitConfig.getUserIds()));
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(new HashSet<>(customerLimitConfig.getDeptIds()));
+        // 拼接数据
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(customerLimitConfig.getUserIds());
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(customerLimitConfig.getDeptIds());
         return success(CrmCustomerLimitConfigConvert.INSTANCE.convert(customerLimitConfig, userMap, deptMap));
     }
 
@@ -84,10 +84,14 @@ public class CrmCustomerLimitConfigController {
     @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:query')")
     public CommonResult<PageResult<CrmCustomerLimitConfigRespVO>> getCustomerLimitConfigPage(@Valid CrmCustomerLimitConfigPageReqVO pageVO) {
         PageResult<CrmCustomerLimitConfigDO> pageResult = customerLimitConfigService.getCustomerLimitConfigPage(pageVO);
-        Set<Long> userIds = CollectionUtils.convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getUserIds, Collection::stream);
-        Set<Long> deptIds = CollectionUtils.convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getDeptIds, Collection::stream);
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(deptIds);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty(pageResult.getTotal()));
+        }
+        // 拼接数据
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getUserIds, Collection::stream));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+                convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getDeptIds, Collection::stream));
         return success(CrmCustomerLimitConfigConvert.INSTANCE.convertPage(pageResult, userMap, deptMap));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
index fb2325fac..6bd1da362 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
@@ -34,6 +34,7 @@ public class CrmCustomerPoolConfigController {
         return success(CrmCustomerConvert.INSTANCE.convert(customerPoolConfig));
     }
 
+    // TODO @wanwan:这个请求,搞成 save 哈;
     @PutMapping("/update")
     @Operation(summary = "更新客户公海规则设置")
     @PreAuthorize("@ss.hasPermission('crm:customer-pool-config:update')")
@@ -41,4 +42,5 @@ public class CrmCustomerPoolConfigController {
         customerPoolConfigService.updateCustomerPoolConfig(updateReqVO);
         return success(true);
     }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java
index f429dc12e..7b163913e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java
@@ -6,6 +6,7 @@ import lombok.Data;
 import javax.validation.constraints.NotNull;
 import java.util.List;
 
+// TODO @wanwan:vo 下,可以新建一个 limitconfig,放它的 vo;
 /**
  * 客户限制配置 Base VO,提供给添加、修改、详细的子 VO 使用
  * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigBaseVO.java
index 60e6c1b3c..0f6b6f33d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigBaseVO.java
@@ -12,6 +12,7 @@ import javax.validation.constraints.NotNull;
 @Data
 public class CrmCustomerPoolConfigBaseVO {
 
+    // TODO @wanwan:参数校验
     @Schema(description = "是否启用客户公海", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     @NotNull(message = "是否启用客户公海不能为空")
     private Boolean enabled;
@@ -27,4 +28,5 @@ public class CrmCustomerPoolConfigBaseVO {
 
     @Schema(description = "提前提醒天数", example = "2")
     private Integer notifyDays;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigRespVO.java
index 965a337b0..c5af3cad2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPoolConfigRespVO.java
@@ -5,6 +5,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
+// TODO @wanwan:vo 下,可以新建一个 poolconfig,放它的 vo;
 @Schema(description = "管理后台 - CRM 客户公海规则 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index d1e02ae91..ae854b4d0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.crm.convert.customer;
 
-import cn.hutool.core.lang.tree.Node;
 import cn.hutool.core.util.NumberUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
@@ -18,8 +17,6 @@ import org.mapstruct.factory.Mappers;
 
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
 
 /**
  * 客户 Convert
@@ -65,4 +62,5 @@ public interface CrmCustomerConvert {
     CrmCustomerPoolConfigRespVO convert(CrmCustomerPoolConfigDO customerPoolConfig);
 
     CrmCustomerPoolConfigDO convert(CrmCustomerPoolConfigUpdateReqVO updateReqVO);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customerlimitconfig/CrmCustomerLimitConfigConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerLimitConfigConvert.java
similarity index 95%
rename from yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customerlimitconfig/CrmCustomerLimitConfigConvert.java
rename to yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerLimitConfigConvert.java
index 8c8d02b84..33b0cae20 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customerlimitconfig/CrmCustomerLimitConfigConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerLimitConfigConvert.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.convert.customerlimitconfig;
+package cn.iocoder.yudao.module.crm.convert.customer;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
@@ -57,9 +57,11 @@ public interface CrmCustomerLimitConfigConvert {
      * @param respVo 响应实体
      */
     static void fillNameField(Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap, CrmCustomerLimitConfigRespVO respVo) {
+        // TODO wanwan:返回 list,具体怎么拼接叫给前端;
         respVo.setUserNames(respVo.getUserIds().stream().map(userMap::get)
                 .filter(Objects::nonNull).map(AdminUserRespDTO::getNickname).collect(Collectors.joining(",")));
         respVo.setDeptNames(respVo.getDeptIds().stream().map(deptMap::get)
                 .filter(Objects::nonNull).map(DeptRespDTO::getName).collect(Collectors.joining(",")));
     }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerPoolConfigDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerPoolConfigDO.java
index cec0a05bc..7e3b4cd15 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerPoolConfigDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerPoolConfigDO.java
@@ -46,4 +46,5 @@ public class CrmCustomerPoolConfigDO extends BaseDO {
      * 提前提醒天数
      */
     private Integer notifyDays;
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/package-info.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/package-info.java
deleted file mode 100644
index 78e98bcd9..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * 客户
- */
-package cn.iocoder.yudao.module.crm.dal.dataobject.customer;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customerlimitconfig/CrmCustomerLimitConfigDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customerlimitconfig/CrmCustomerLimitConfigDO.java
index d5ed80d7a..5be934b04 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customerlimitconfig/CrmCustomerLimitConfigDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customerlimitconfig/CrmCustomerLimitConfigDO.java
@@ -32,6 +32,8 @@ public class CrmCustomerLimitConfigDO extends BaseDO {
     private Long id;
     /**
      * 规则类型
+     *
+     * TODO @wanwan:搞个枚举哈;
      */
     private Integer type;
     /**
@@ -49,7 +51,9 @@ public class CrmCustomerLimitConfigDO extends BaseDO {
      */
     private Integer maxCount;
     /**
-     * 成交客户是否占有拥有客户数(当 type = 1 时)
+     * 成交客户是否占有拥有客户数
+     *
+     * 当且仅当 {@link #type} 为 1 时,进行使用
      */
     private Boolean dealCountEnabled;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigService.java
index cba8f87cd..30e3873ab 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigService.java
@@ -25,4 +25,5 @@ public interface CrmCustomerPoolConfigService {
      * @param saveReqVO 更新信息
      */
     void updateCustomerPoolConfig(@Valid CrmCustomerPoolConfigUpdateReqVO saveReqVO);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
index 1ea6f98b4..1a08adbf5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerPoolConfigServiceImpl.java
@@ -44,6 +44,7 @@ public class CrmCustomerPoolConfigServiceImpl implements CrmCustomerPoolConfigSe
      */
     @Override
     public void updateCustomerPoolConfig(CrmCustomerPoolConfigUpdateReqVO saveReqVO) {
+        // TODO @wanwan:看下 @AssertTrue 的逻辑;
         if (BooleanUtil.isTrue(saveReqVO.getEnabled()) && (ObjectUtil.hasNull(saveReqVO.getContactExpireDays(), saveReqVO.getDealExpireDays()))) {
             throw exception(CUSTOMER_POOL_CONFIG_ERROR);
         }
@@ -60,4 +61,5 @@ public class CrmCustomerPoolConfigServiceImpl implements CrmCustomerPoolConfigSe
         // 不存在,则进行插入
         customerPoolConfigMapper.insert(CrmCustomerConvert.INSTANCE.convert(saveReqVO));
     }
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
index 2a6a3ca5d..9f3f3087f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImpl.java
@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigUpdateReqVO;
-import cn.iocoder.yudao.module.crm.convert.customerlimitconfig.CrmCustomerLimitConfigConvert;
+import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerLimitConfigConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.customerlimitconfig.CrmCustomerLimitConfigMapper;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/business/CrmBusinessMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/business/CrmBusinessMapper.xml
deleted file mode 100644
index c35aafec7..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/business/CrmBusinessMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/businessstatustype/CrmBusinessStatusTypeMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/businessstatustype/CrmBusinessStatusTypeMapper.xml
deleted file mode 100644
index 44fbd07ec..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/businessstatustype/CrmBusinessStatusTypeMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.businessstatustype.CrmBusinessStatusTypeMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
deleted file mode 100644
index 3f15cf766..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/clue/CrmClueMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/contact/ContactMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/contact/ContactMapper.xml
deleted file mode 100644
index e2c69ca78..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/contact/ContactMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerMapper.xml
deleted file mode 100644
index 85eb90d07..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerPoolConfigMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerPoolConfigMapper.xml
deleted file mode 100644
index 0b7097713..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customer/CrmCustomerPoolConfigMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerPoolConfigMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customerlimitconfig/CrmCustomerLimitConfigMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customerlimitconfig/CrmCustomerLimitConfigMapper.xml
deleted file mode 100644
index c9eefc4e5..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/customerlimitconfig/CrmCustomerLimitConfigMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.customerlimitconfig.CrmCustomerLimitConfigMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/product/ProductMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/product/ProductMapper.xml
deleted file mode 100644
index f1a52d2f1..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/product/ProductMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.product.ProductMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/productcategory/ProductCategoryMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/productcategory/ProductCategoryMapper.xml
deleted file mode 100644
index f3b4d0d4a..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/productcategory/ProductCategoryMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.productcategory.ProductCategoryMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivableMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivableMapper.xml
deleted file mode 100644
index b2ab2042e..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivableMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivablePlanMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivablePlanMapper.xml
deleted file mode 100644
index 3b18148de..000000000
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/receivable/CrmReceivablePlanMapper.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper">
-
-    <!--
-        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
-        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
-        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
-        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
-     -->
-
-</mapper>
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImplTest.java
index 95fd8ceeb..827fd0e02 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImplTest.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/customerlimitconfig/CrmCustomerLimitConfigServiceImplTest.java
@@ -20,6 +20,7 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_LIMIT_CONFIG_NOT_EXISTS;
 import static org.junit.jupiter.api.Assertions.*;
 
+// TODO 芋艿:单测后面搞
 /**
  * {@link CrmCustomerLimitConfigServiceImpl} 的单元测试类
  *
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java
index c3d143e46..f8059fbb7 100644
--- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dept/DeptApi.java
@@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 /**
  * 部门 API 接口
@@ -46,7 +45,7 @@ public interface DeptApi {
      * @param ids 部门编号数组
      * @return 部门 Map
      */
-    default Map<Long, DeptRespDTO> getDeptMap(Set<Long> ids) {
+    default Map<Long, DeptRespDTO> getDeptMap(Collection<Long> ids) {
         List<DeptRespDTO> list = getDeptList(ids);
         return CollectionUtils.convertMap(list, DeptRespDTO::getId);
     }
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
index 03644b30f..f8c817db5 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
@@ -109,6 +109,7 @@ public class UserController {
         return success(new PageResult<>(userList, pageResult.getTotal()));
     }
 
+    // TODO @芋艿:看看这里怎么统一调整下;客户的选择组件;
     @GetMapping("/all")
     @Operation(summary = "查询所有用户列表")
     public CommonResult<List<UserPageItemRespVO>> getAllUser() {

From 716b081464dae1ffd507b317326b0fadde64645b Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 18 Nov 2023 22:31:05 +0800
Subject: [PATCH 094/101] =?UTF-8?q?crm=EF=BC=9A=E5=90=88=E5=B9=B6=E6=9C=80?=
 =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/contact/ContactController.java         |  3 ++-
 .../admin/customer/CrmCustomerController.java    | 15 +++++++--------
 .../crm/convert/contact/ContactConvert.java      |  9 ++++-----
 .../crm/convert/customer/CrmCustomerConvert.java |  7 +------
 .../dal/dataobject/customer/CrmCustomerDO.java   |  6 ++++++
 .../crm/service/contact/ContactService.java      | 16 +++++++++++-----
 .../crm/service/contact/ContactServiceImpl.java  | 11 ++++++++++-
 .../module/system/api/dept/PostApiImpl.java      |  6 ++++--
 8 files changed, 45 insertions(+), 28 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
index 3a5406419..bf376ffa3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
@@ -34,6 +34,7 @@ import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 // TODO @zya:crm 所有的类,dou带 Crm 前缀,因为它的名字太通用了,可能和后续的 erp 之类的冲突
 @Tag(name = "管理后台 - CRM 联系人")
@@ -55,7 +56,7 @@ public class ContactController {
     @Operation(summary = "创建联系人")
     @PreAuthorize("@ss.hasPermission('crm:contact:create')")
     public CommonResult<Long> createContact(@Valid @RequestBody ContactCreateReqVO createReqVO) {
-        return success(contactService.createContact(createReqVO));
+        return success(contactService.createContact(createReqVO, getLoginUserId()));
     }
 
     @PutMapping("/update")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index bfa35f0eb..43d1007b0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
@@ -33,8 +34,6 @@ import java.util.Map;
 import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@@ -111,16 +110,16 @@ public class CrmCustomerController {
     @Operation(summary = "获得客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
-        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO);
+        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO, getLoginUserId());
         if (CollUtil.isEmpty(pageResult.getList())) {
             return success(PageResult.empty(pageResult.getTotal()));
         }
         // 拼接数据
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                convertSetByFlatMap(pageResult.getList(), user -> Stream.of(NumberUtil.parseLong(user.getCreator()), user.getOwnerUserId())));
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
-                convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
-        return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap));
+        // TODO 芋艿:需要 review 下;
+//        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+//                convertSetByFlatMap(pageResult.getList(), user -> Stream.of(NumberUtil.parseLong(user.getCreator()), user.getOwnerUserId())));
+//        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+//                convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
         return convertPage(customerService.getCustomerPage(pageVO, getLoginUserId()));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
index ae97dde21..a5be7d84f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/ContactConvert.java
@@ -1,16 +1,15 @@
 package cn.iocoder.yudao.module.crm.convert.contact;
 
-import java.util.*;
-
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+
+import java.util.List;
 
 /**
  * crm联系人 Convert
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index a02269fc4..83e75610d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -4,13 +4,9 @@ import cn.hutool.core.util.NumberUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
-import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
 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.CrmTransferPermissionReqBO;
-import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
-import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -24,7 +20,6 @@ import java.util.List;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
-import java.util.Map;
 
 /**
  * 客户 Convert
@@ -70,7 +65,7 @@ public interface CrmCustomerConvert {
             MapUtils.findAndThen(userMap, customerRespVO.getOwnerUserId(), ownerUser -> {
                 customerRespVO.setOwnerUserName(ownerUser.getNickname());
                 MapUtils.findAndThen(deptMap, ownerUser.getDeptId(), dept ->
-                        customerRespVO.setOwnerUserDept(dept.getName()));
+                        customerRespVO.setOwnerUserDeptName(dept.getName()));
             });
         });
         return result;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
index 6e50b43de..c5826b7c3 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
@@ -97,6 +97,12 @@ public class CrmCustomerDO extends BaseDO {
      * 备注
      */
     private String remark;
+    /**
+     * 负责人的用户编号
+     *
+     * 关联 AdminUserDO 的 id 字段
+     */
+    private Long ownerUserId;
     /**
      * 地区编号
      */
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
index 4e835f0e7..c27c1541a 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactService.java
@@ -1,10 +1,15 @@
 package cn.iocoder.yudao.module.crm.service.contact;
 
-import java.util.*;
-import javax.validation.*;
-import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactExportReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
 
 /**
  * crm联系人 Service 接口
@@ -17,9 +22,10 @@ public interface ContactService {
      * 创建crm联系人
      *
      * @param createReqVO 创建信息
+     * @param userId 用户编号
      * @return 编号
      */
-    Long createContact(@Valid ContactCreateReqVO createReqVO);
+    Long createContact(@Valid ContactCreateReqVO createReqVO, Long userId);
 
     /**
      * 更新crm联系人
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
index a594a247f..279bbc568 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/ContactServiceImpl.java
@@ -10,7 +10,13 @@ import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.ContactUpdateReqV
 import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contact.ContactMapper;
+import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
@@ -32,8 +38,11 @@ public class ContactServiceImpl implements ContactService {
     @Resource
     private ContactMapper contactMapper;
 
+    @Resource
+    private CrmPermissionService crmPermissionService;
+
     @Override // TODO @zyna:新增和修改时,关联字段要校验,例如说 直属上级,是不是真的存在;
-    public Long createContact(ContactCreateReqVO createReqVO) {
+    public Long createContact(ContactCreateReqVO createReqVO, Long userId) {
         // 插入
         ContactDO contact = ContactConvert.INSTANCE.convert(createReqVO);
         contactMapper.insert(contact);
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java
index 3e19395d5..e61b19e21 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dept/PostApiImpl.java
@@ -1,7 +1,8 @@
 package cn.iocoder.yudao.module.system.api.dept;
 
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
-import cn.iocoder.yudao.module.system.convert.dept.PostConvert;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
 import cn.iocoder.yudao.module.system.service.dept.PostService;
 import org.springframework.stereotype.Service;
 
@@ -27,7 +28,8 @@ public class PostApiImpl implements PostApi {
 
     @Override
     public List<PostRespDTO> getPostList(Collection<Long> ids) {
-        return PostConvert.INSTANCE.convert(postService.getPostList(ids));
+        List<PostDO> list = postService.getPostList(ids);
+        return BeanUtils.toBean(list, PostRespDTO.class);
     }
 
 }

From 58ede7d69c013f43623f1815e15b34aab5662bbd Mon Sep 17 00:00:00 2001
From: liuhongfeng <291117974@qq.com>
Date: Sat, 18 Nov 2023 23:30:24 +0800
Subject: [PATCH 095/101] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E3=80=91?=
 =?UTF-8?q?=E6=9C=80=E6=96=B0=E7=9A=84SQL=E5=AD=97=E7=AC=A6=E7=BC=96?=
 =?UTF-8?q?=E7=A0=81=E4=B8=8D=E4=B8=80=E8=87=B4=E5=AF=BC=E8=87=B4=E5=88=9B?=
 =?UTF-8?q?=E5=BB=BA=E8=A1=A8=E5=A4=B1=E8=B4=A5=EF=BC=8C=E4=BF=AE=E6=94=B9?=
 =?UTF-8?q?utf8mb4=5F0900=5Fai=5Fci=3D=E3=80=8Butf8mb4=5Funicode=5Fci?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mysql/ruoyi-vue-pro.sql | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql
index a9cca3ba8..9fec3ee01 100644
--- a/sql/mysql/ruoyi-vue-pro.sql
+++ b/sql/mysql/ruoyi-vue-pro.sql
@@ -535,8 +535,8 @@ CREATE TABLE `infra_demo01_contact`  (
   `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
   `sex` tinyint(1) NOT NULL COMMENT '性别',
   `birthday` datetime NOT NULL COMMENT '出生年',
-  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
-  `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
+  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '简介',
+  `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
@@ -651,7 +651,7 @@ CREATE TABLE `infra_demo03_student`  (
   `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
   `sex` tinyint NOT NULL COMMENT '性别',
   `birthday` datetime NOT NULL COMMENT '出生日期',
-  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
+  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '简介',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',

From 86050d411b47d8c08d46e684def21e89c2c901ea Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 18 Nov 2023 23:45:24 +0800
Subject: [PATCH 096/101] =?UTF-8?q?code=20review=EF=BC=9A=E6=9D=83?=
 =?UTF-8?q?=E9=99=90=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../crm/enums/customer/CrmCustomerSceneEnum.java     |  1 +
 .../admin/business/vo/CrmBusinessTransferReqVO.java  |  3 ++-
 .../admin/contact/vo/CrmContactTransferReqVO.java    |  3 ++-
 .../admin/contract/vo/CrmContractTransferReqVO.java  |  3 ++-
 .../admin/customer/CrmCustomerController.java        |  4 +++-
 .../admin/customer/vo/CrmCustomerPageReqVO.java      |  4 +++-
 .../admin/customer/vo/CrmCustomerTransferReqVO.java  |  3 ++-
 .../admin/permission/CrmPermissionController.java    | 12 ++++++++++--
 .../admin/permission/vo/CrmPermissionBaseVO.java     |  8 ++++----
 .../admin/permission/vo/CrmPermissionRespVO.java     |  6 +++---
 .../crm/convert/customer/CrmCustomerConvert.java     |  1 +
 .../crm/convert/permission/CrmPermissionConvert.java |  1 +
 .../dal/dataobject/permission/CrmPermissionDO.java   |  1 +
 .../crm/dal/mysql/business/CrmBusinessMapper.java    |  1 +
 .../framework/core/annotations/CrmPermission.java    | 11 ++++++-----
 .../crm/framework/core/aop/CrmPermissionAspect.java  |  2 ++
 .../module/crm/framework/core/config/SpelConfig.java |  1 +
 .../module/crm/framework/enums/CrmBizTypeEnum.java   |  1 +
 .../crm/service/business/CrmBusinessServiceImpl.java |  1 +
 .../crm/service/customer/CrmCustomerServiceImpl.java |  1 +
 .../service/permission/CrmPermissionServiceImpl.java |  9 +++++----
 .../permission/bo/CrmPermissionTransferReqBO.java    |  5 +++--
 .../permission/bo/CrmPermissionUpdateReqBO.java      |  2 +-
 23 files changed, 57 insertions(+), 27 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java
index 1bb374081..81cb674eb 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java
@@ -7,6 +7,7 @@ import lombok.Getter;
 
 import java.util.Arrays;
 
+// TODO @puhui999:这个应该是 crm 全局的,不仅仅属于 customer 客户哈;
 /**
  * CRM 客户等级
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
index db24abcf8..b5a1153f7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
@@ -17,12 +17,13 @@ public class CrmBusinessTransferReqVO {
     /**
      * 新负责人的用户编号
      */
-    @NotNull(message = "新负责人的用户编号不能为空")
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
     private Long newOwnerUserId;
 
     /**
      * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
      * 关联 {@link CrmPermissionLevelEnum}
      */
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
index 32bdcd754..2acc26a97 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
@@ -17,12 +17,13 @@ public class CrmContactTransferReqVO {
     /**
      * 新负责人的用户编号
      */
-    @NotNull(message = "新负责人的用户编号不能为空")
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
     private Long newOwnerUserId;
 
     /**
      * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
      * 关联 {@link CrmPermissionLevelEnum}
      */
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
index 95691b5a8..4ebef5943 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
@@ -17,12 +17,13 @@ public class CrmContractTransferReqVO {
     /**
      * 新负责人的用户编号
      */
-    @NotNull(message = "新负责人的用户编号不能为空")
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
     private Long newOwnerUserId;
 
     /**
      * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
      * 关联 {@link CrmPermissionLevelEnum}
      */
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 43d1007b0..103e2c34e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -106,6 +106,7 @@ public class CrmCustomerController {
         return success(CrmCustomerConvert.INSTANCE.convert(customer, ownerMap, userMap, deptMap));
     }
 
+    // TODO @puhui999:可以在 CrmCustomerPageReqVO 里面加个 pool 参数,为 true 时,代表来自公海客户的分页
     @GetMapping("/page")
     @Operation(summary = "获得客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
@@ -115,7 +116,7 @@ public class CrmCustomerController {
             return success(PageResult.empty(pageResult.getTotal()));
         }
         // 拼接数据
-        // TODO 芋艿:需要 review 下;
+        // TODO @puhui999:这块的拼接逻辑,可以和 convertPage 合并下;
 //        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
 //                convertSetByFlatMap(pageResult.getList(), user -> Stream.of(NumberUtil.parseLong(user.getCreator()), user.getOwnerUserId())));
 //        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
@@ -123,6 +124,7 @@ public class CrmCustomerController {
         return convertPage(customerService.getCustomerPage(pageVO, getLoginUserId()));
     }
 
+    // TODO @puhui999:
     @GetMapping("/pool-page")
     @Operation(summary = "获得公海客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
index 5f8b9f28e..fafe3d770 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java
@@ -29,7 +29,9 @@ public class CrmCustomerPageReqVO extends PageParam {
     private Integer source;
 
     /**
-     * 场景类型,关联 {@link CrmCustomerSceneEnum}
+     * 场景类型
+     *
+     * 关联 {@link CrmCustomerSceneEnum}
      */
     @Schema(description = "场景类型", example = "1")
     private Integer sceneType;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
index 23df716c2..ed7cfb5c5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
@@ -17,12 +17,13 @@ public class CrmCustomerTransferReqVO {
     /**
      * 新负责人的用户编号
      */
-    @NotNull(message = "新负责人的用户编号不能为空")
     @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
     private Long newOwnerUserId;
 
     /**
      * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
      * 关联 {@link CrmPermissionLevelEnum}
      */
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
index a13694e1a..75e8ad658 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
@@ -55,16 +55,18 @@ public class CrmPermissionController {
     @Resource
     private PostApi postApi;
 
+    // TODO @puhui999:保持统一,create 噢;然后是 PostMapping
     @PutMapping("/add")
     @Operation(summary = "添加团队成员")
     @PreAuthorize("@ss.hasPermission('crm:permission:create')")
-    @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#reqVO.bizType", bizId = "#reqVO.bizId"
-            , level = CrmPermissionLevelEnum.OWNER)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#reqVO.bizType", bizId = "#reqVO.bizId",
+            level = CrmPermissionLevelEnum.OWNER)
     public CommonResult<Boolean> addPermission(@Valid @RequestBody CrmPermissionCreateReqVO reqVO) {
         permissionService.createPermission(CrmPermissionConvert.INSTANCE.convert(reqVO));
         return success(true);
     }
 
+    // TODO @puhui999:领取公海客户,是不是放到客户那更合适哈?
     @PutMapping("/receive")
     @Operation(summary = "领取公海数据")
     @PreAuthorize("@ss.hasPermission('crm:permission:update')")
@@ -73,6 +75,7 @@ public class CrmPermissionController {
         return success(true);
     }
 
+    // TODO @puhui999:是不是放到客户那更合适哈?
     @PutMapping("/put-pool")
     @Operation(summary = "数据放入公海")
     @PreAuthorize("@ss.hasPermission('crm:permission:update')")
@@ -93,6 +96,7 @@ public class CrmPermissionController {
         return success(true);
     }
 
+    // TODO @puhui999:bizType 和 bizId 是不是不用啦;因为参数校验需要 bizType 和 bizId,可以先查询下,在直接调用方法;不一定都要注解哈;
     @DeleteMapping("/delete")
     @Operation(summary = "移除团队成员")
     @Parameters({
@@ -110,9 +114,11 @@ public class CrmPermissionController {
         return success(true);
     }
 
+    // TODO @puhui999:deleteSelfPermission;尽量归成 crud 这样的操作哈;
     @DeleteMapping("/quit-team")
     @Operation(summary = "退出团队")
     @Parameters({
+            // TODO @puhui999:这个可以拿出来,不用包在 @Parameters 里,在只有一个参数时哈;
             @Parameter(name = "id", description = "团队成员编号", required = true, example = "1024")
     })
     @PreAuthorize("@ss.hasPermission('crm:permission:delete')")
@@ -143,6 +149,7 @@ public class CrmPermissionController {
         }
         // TODO @puhui999:池子的逻辑;
         // 判断是否是公海数据
+        // TODO @puhui999:这段逻辑,可以删除么?
         Predicate<CrmPermissionDO> filter = item -> ObjUtil.equal(item.getUserId(), CrmPermissionDO.POOL_USER_ID);
         if (anyMatch(permission, filter)) {
             permission.removeIf(filter); // 排除
@@ -151,6 +158,7 @@ public class CrmPermissionController {
         // 拼接数据
         List<AdminUserRespDTO> userList = adminUserApi.getUserList(convertSet(permission, CrmPermissionDO::getUserId));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userList, AdminUserRespDTO::getDeptId));
+        // TODO @puhui999:CollectionUtils.convertSetByFlatMap() 看看可以不
         Set<Long> postIds = userList.stream().flatMap(item -> item.getPostIds().stream()).collect(Collectors.toSet());
         Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
         return success(CrmPermissionConvert.INSTANCE.convert(permission, userList, deptMap, postMap));
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
index 4ad70859f..595b8f9f2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java
@@ -21,13 +21,13 @@ public class CrmPermissionBaseVO {
     @NotNull(message = "用户编号不能为空")
     private Long userId;
 
-    @Schema(description = "Crm 类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @Schema(description = "CRM 类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     @InEnum(CrmBizTypeEnum.class)
-    @NotNull(message = "Crm 类型不能为空")
+    @NotNull(message = "CRM 类型不能为空")
     private Integer bizType;
 
-    @Schema(description = "Crm 类型数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
-    @NotNull(message = "Crm 类型数据编号不能为空")
+    @Schema(description = "CRM 类型数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "CRM 类型数据编号不能为空")
     private Long bizId;
 
     @Schema(description = "权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
index acd7003a0..9440a949f 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
@@ -13,12 +13,12 @@ public class CrmPermissionRespVO extends CrmPermissionBaseVO {
     @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     private Long id;
 
-    @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部")
-    private String deptName;
-
     @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
     private String nickname;
 
+    @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部")
+    private String deptName;
+
     @Schema(description = "岗位名称数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[BOOS,经理]")
     private Set<String> postNames;
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
index 83e75610d..732a59f84 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
@@ -81,6 +81,7 @@ public interface CrmCustomerConvert {
 
     PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> page);
 
+    // TODO @puhui999:两个 convertPage 的逻辑,合并下;
     default PageResult<CrmCustomerRespVO> convertPage(PageResult<CrmCustomerDO> pageResult, Map<Long, CrmPermissionDO> ownerMap,
                                                       Map<Long, AdminUserRespDTO> userMap, Map<Long, DeptRespDTO> deptMap) {
         PageResult<CrmCustomerRespVO> result = convertPage(pageResult);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
index 2b8e321d6..42f784ba7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/permission/CrmPermissionConvert.java
@@ -56,6 +56,7 @@ public interface CrmPermissionConvert {
     }
 
     default List<CrmPermissionDO> convertList(CrmPermissionUpdateReqVO updateReqVO) {
+        // TODO @puhui999:CollectionUtils.convert
         List<CrmPermissionDO> permissions = new ArrayList<>();
         updateReqVO.getIds().forEach(id -> {
             permissions.add(new CrmPermissionDO().setId(id).setLevel(updateReqVO.getLevel()));
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
index aa3b1e64f..55d583597 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/permission/CrmPermissionDO.java
@@ -24,6 +24,7 @@ import lombok.*;
 public class CrmPermissionDO extends BaseDO {
 
     // TODO puhui999:是不是公海的数据,就不插入了;这样方便获取公海数据鸭
+    // TODO @puhui999:每个数据那的负责人,我想了下,还是存储的;
     /**
      * 当数据变为公海数据时,也就是数据团队成员中没有负责人的时候,将原本的负责人 userId 设置为 POOL_USER_ID 方便查询公海数据。
      * 也就是说每条数据到最后都有一个负责人,如果有人领取则 userId 为领取人
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
index f8f856104..760a52593 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
@@ -26,6 +26,7 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
                 .orderByDesc(CrmBusinessDO::getId));
     }
 
+    // TODO @puhui999:selectList 噢;
     default List<CrmBusinessDO> selectPage(CrmBusinessExportReqVO reqVO) {
         return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
                 .likeIfPresent(CrmBusinessDO::getName, reqVO.getName())
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
index 5ef382b74..09526e070 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java
@@ -12,7 +12,7 @@ import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
 import static java.lang.annotation.ElementType.METHOD;
 
 /**
- * Crm 数据操作权限校验 AOP 注解
+ * CRM 数据操作权限校验 AOP 注解
  *
  * @author HUIHUI
  */
@@ -22,18 +22,19 @@ import static java.lang.annotation.ElementType.METHOD;
 public @interface CrmPermission {
 
     /**
-     * crm 类型
+     * CRM 类型
      */
     CrmBizTypeEnum bizType();
 
     /**
-     * crm 类型扩展
-     * 用于 CrmPermissionController 团队权限校验
+     * CRM 类型扩展,通过 Spring EL 表达式获取到 {@link #bizType()}
+     *
+     * 目的:用于 CrmPermissionController 团队权限校验
      */
     String bizTypeValue() default "";
 
     /**
-     * 数据编号,通过 spring el 表达式获取
+     * 数据编号,通过 Spring EL 表达式获取
      * TODO 数据权限完成后去除 default ""
      */
     String bizId() default "";
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
index 251a921ff..d1d30dca4 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java
@@ -108,6 +108,8 @@ public class CrmPermissionAspect {
         throw exception(CRM_PERMISSION_DENIED, crmPermission.bizType().getName());
     }
 
+
+    // TODO @puhui999:这块看看能不能用 SpringExpressionUtils 工具类;
     private KeyValue<Long, Integer> getBizIdAndBizType(JoinPoint joinPoint, CrmPermission crmPermission) throws NoSuchMethodException {
         Method method = getMethod(joinPoint);
         // 1. 获取方法的参数值
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/config/SpelConfig.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/config/SpelConfig.java
index 2bdfcbe8b..efbf5d1d5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/config/SpelConfig.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/config/SpelConfig.java
@@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 
+// TODO @puhui999:SpringExpressionUtils
 /**
  * 注册 Spel 所需 Bean
  *
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
index 553121243..e4fc3dbbf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
@@ -17,6 +17,7 @@ import java.util.Arrays;
 @Getter
 public enum CrmBizTypeEnum implements IntArrayValuable {
 
+    // TODO @puhui999:如果类似 CrmBizPermission 的 bizType 需要为空,可以设置它是数组,参考 Telephone 的 payload
     CRM_PERMISSION(0, "团队"), // CrmPermissionController 中使用
     CRM_LEADS(1, "线索"),
     CRM_CUSTOMER(2, "客户"),
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index bb154d056..fc322fac5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -103,6 +103,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     @Override
     public PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO, Long userId) {
         // 1. 获取当前用户能看的分页数据
+        // TODO @puhui999:如果业务的数据量比较大,in 太多可能有性能问题噢;看看是不是搞成 join 连表了;可以微信讨论下;
         List<CrmPermissionDO> permissions = crmPermissionService.getPermissionListByBizTypeAndUserId(
                 CrmBizTypeEnum.CRM_BUSINESS.getType(), userId);
         Set<Long> ids = convertSet(permissions, CrmPermissionDO::getBizId);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 5d56211d6..a461440e0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -100,6 +100,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
             return customerMapper.selectPage(pageReqVO, Collections.emptyList());
         }
         // 1.2 获取当前用户能看的分页数据
+        // TODO @puhui999:如果业务的数据量比较大,in 太多可能有性能问题噢;看看是不是搞成 join 连表了;可以微信讨论下;
         List<CrmPermissionDO> permissions = crmPermissionService.getPermissionListByBizTypeAndUserId(
                 CrmBizTypeEnum.CRM_CUSTOMER.getType(), userId);
         // 1.3 TODO 场景数据过滤
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
index e7efd1284..681c1654b 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -24,8 +24,9 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum.isOwner;
 
+// TODO @puhui999:尽量规避用“团队”这个词哈;这个只是我们给前端展示用的;
 /**
- * crm 数据权限 Service 接口实现类
+ * CRM 数据权限 Service 接口实现类
  *
  * @author HUIHUI
  */
@@ -117,6 +118,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         // 1.2 校验新负责人是否存在
         adminUserApi.validateUserList(Collections.singletonList(transferReqBO.getNewOwnerUserId()));
 
+        // TODO @puhui999:2. 和 2.1 合并成 2;2.2 单独成 3;说白了,就是 2. 修改新负责人的权限;3. 修改老负责人的权限;这样整体注释会简洁一点,也清晰一点;
         // 2. 权限转移
         List<CrmPermissionDO> permissions = crmPermissionMapper.selectByBizTypeAndBizId(
                 transferReqBO.getBizType(), transferReqBO.getBizId()); // 获取所有团队成员
@@ -127,7 +129,6 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
             crmPermissionMapper.insert(new CrmPermissionDO().setBizType(transferReqBO.getBizType())
                     .setBizId(transferReqBO.getBizId()).setUserId(transferReqBO.getNewOwnerUserId())
                     .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
-
         } else { // 存在则修改权限级别
             crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId())
                     .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
@@ -138,7 +139,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
                     .setLevel(transferReqBO.getOldOwnerPermissionLevel())); // 设置加入团队后的级别
             return;
         }
-        crmPermissionMapper.deleteById(oldPermission.getId()); // 移除
+        crmPermissionMapper.deleteById(oldPermission.getId());
     }
 
     @Override
@@ -164,7 +165,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
         if (permission == null) { // 不存在则模块数据也不存在
             throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, CrmBizTypeEnum.getNameByType(bizType));
         }
-
+        // 更新
         crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId()).setUserId(CrmPermissionDO.POOL_USER_ID));
     }
 
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java
index 23da43daf..3e3873b47 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java
@@ -22,7 +22,7 @@ public class CrmPermissionTransferReqBO {
     private Long userId;
 
     /**
-     * Crm 类型
+     * CRM 类型
      */
     @NotNull(message = "Crm 类型不能为空")
     @InEnum(CrmBizTypeEnum.class)
@@ -30,7 +30,7 @@ public class CrmPermissionTransferReqBO {
     /**
      * 数据编号
      */
-    @NotNull(message = "Crm 数据编号不能为空")
+    @NotNull(message = "CRM 数据编号不能为空")
     private Long bizId;
 
     /**
@@ -41,6 +41,7 @@ public class CrmPermissionTransferReqBO {
 
     /**
      * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
      * 关联 {@link CrmPermissionLevelEnum}
      */
     private Integer oldOwnerPermissionLevel;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
index 38eff3ef5..9cd198cdf 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionUpdateReqBO.java
@@ -17,7 +17,7 @@ public class CrmPermissionUpdateReqBO {
     /**
      * 数据权限编号
      */
-    @NotNull(message = "Crm 数据权限编号不能为空")
+    @NotNull(message = "数据权限编号不能为空")
     private Long id;
 
     /**

From f589881de57297e72d4f0067596f36e86cc6c466 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 19 Nov 2023 00:05:06 +0800
Subject: [PATCH 097/101] =?UTF-8?q?code=20review=EF=BC=9A=E5=AE=A2?=
 =?UTF-8?q?=E6=88=B7=E7=9A=84=E5=88=86=E9=85=8D=E5=92=8C=E9=A2=86=E5=8F=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/customer/CrmCustomerController.java   | 11 +++++------
 .../service/customer/CrmCustomerService.java    | 14 ++++++++------
 .../customer/CrmCustomerServiceImpl.java        | 17 ++++++-----------
 3 files changed, 19 insertions(+), 23 deletions(-)

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
index 1f4087486..8a34b599d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -1,9 +1,6 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.NumberUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -185,17 +182,20 @@ public class CrmCustomerController {
     }
 
     @PutMapping("/receive")
-    @Operation(summary = "根据客户id领取公海任务")
+    @Operation(summary = "领取公海客户")
+    // TODO @xiaqing:1)receiveCustomer 方法名字;2)cIds 改成 ids,要加下 @RequestParam,还有 swagger 注解;3)参数非空,使用 validator 校验;4)返回 true 即可;
     @PreAuthorize("@ss.hasPermission('crm:customer:receive')")
-    public CommonResult<String>  receiveByIds(List<Long> cIds){
+    public CommonResult<String> receiveByIds(List<Long> cIds){
         // 判断是否为空
         if(CollectionUtils.isEmpty(cIds))
             return error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),GlobalErrorCodeConstants.BAD_REQUEST.getMsg());
         // 领取公海任务
+        // TODO @xiaqing:userid,通过 controller 传递给 service,不要在 service 里面获取,无状态
         customerService.receive(cIds);
         return success("领取成功");
     }
 
+    // TODO @xiaqing:1)distributeCustomer 方法名;2)cIds 同上;3)参数校验,同上;4)ownerId 改成 ownerUserId,和别的模块统一;5)返回 true 即可;
     @PutMapping("/distributeByIds")
     @Operation(summary = "分配公海给对应负责人")
     @PreAuthorize("@ss.hasPermission('crm:customer:distributeByIds')")
@@ -207,5 +207,4 @@ public class CrmCustomerController {
         return success("分配成功");
     }
 
-
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index a7ab871d5..6aa21477e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -85,20 +85,22 @@ public interface CrmCustomerService {
      */
     void lockCustomer(@Valid CrmCustomerUpdateReqVO updateReqVO);
 
+    // TODO @xiaqing:根据 controller 的建议,改下
     /**
-     * 描述    :接受公海客户
-     * Author :xiaqing
-     * Date   :2023-11-07 22:47:40
+     * 领取公海客户
+     *
+     * @param ids 要领取的客户 id
      */
     void receive(List<Long>ids);
 
+    // TODO @xiaqing:根据 controller 的建议,改下
     /**
+     * 分配公海客户
      *
-     *功能描述: 分配负责人
-     * @param cIds 要分配的客户id
+     * @param cIds 要分配的客户 id
      * @param ownerId 分配的负责人id
      * @author xiaqing
-     * @date 2023-11-08 10:40:22
      */
     void distributeByIds(List<Long>cIds,Long ownerId);
+
 }
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index 04aff2714..ebb499128 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -21,16 +21,10 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.util.Collections;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
 
 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.CUSTOMER_NOT_EXISTS;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 
 /**
@@ -178,7 +172,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
     }
 
     private void transferCustomerOwner(List <Long> cIds, Long ownerId){
-        //先一次性校验完成客户是否可用
+        // 先一次性校验完成客户是否可用
+        // TODO @xiaqing:批量一次性加载客户列表,然后去逐个校验;
         for (Long cId : cIds) {
             //校验是否存在
             validateCustomerExists(cId);
@@ -189,13 +184,14 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
             //todo 校验成交状态
             validCustomerDeal(cId);
         }
+        // TODO @xiaqing:每个客户更新的时候,where 条件,加上 owner_user_id is null,防止并发问题;
         List<CrmCustomerDO> updateDos = new ArrayList <>();
         for (Long cId : cIds){
             CrmCustomerDO customerDO = new CrmCustomerDO();
             customerDO.setId(cId);
             customerDO.setOwnerUserId(SecurityFrameworkUtils.getLoginUserId());
         }
-        //统一修改状态
+        // 统一修改状态
         customerMapper.updateBatch(updateDos);
     }
 
@@ -213,9 +209,8 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
 
     private void validCustomerDeal(Long id) {
         if (customerMapper.selectById(id).getDealStatus() ==true) {
-            throw exception(CUSTOMER_DEALED);
+            throw exception(CUSTOMER_ALREADY_DEAL);
         }
     }
 
-
 }

From 6270975902665bf248b88a50e5480adcd444c341 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 19 Nov 2023 00:37:30 +0800
Subject: [PATCH 098/101] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=90=AF=E5=8A=A8?=
 =?UTF-8?q?=E9=80=9F=E5=BA=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pom.xml              |  2 +-
 yudao-server/pom.xml | 10 +++++-----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/pom.xml b/pom.xml
index 828fb89c8..bcffebfd7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
 <!--        <module>yudao-module-mp</module>-->
 <!--        <module>yudao-module-pay</module>-->
 <!--        <module>yudao-module-mall</module>-->
-        <module>yudao-module-crm</module>
+<!--        <module>yudao-module-crm</module>-->
         <!-- 示例项目 -->
 <!--        <module>yudao-example</module>-->
     </modules>
diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index f58aa5bd0..7af09761f 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -92,11 +92,11 @@
 <!--        </dependency>-->
 
         <!-- CRM 相关模块。默认注释,保证编译速度 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-crm-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-crm-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
 
         <!-- spring boot 配置所需依赖 -->
         <dependency>

From c7a21b2a6447a547c6470a26180cbee6a2bb39bf Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 19 Nov 2023 00:58:12 +0800
Subject: [PATCH 099/101] =?UTF-8?q?mall=EF=BC=9A=E4=BC=98=E5=8C=96?=
 =?UTF-8?q?=E5=95=86=E5=93=81=E6=94=B6=E8=97=8F=E7=9A=84=E5=88=86=E9=A1=B5?=
 =?UTF-8?q?=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mybatis/core/mapper/BaseMapperX.java      |  1 +
 .../favorite/ProductFavoriteController.java   | 19 +++++----------
 .../favorite/vo/ProductFavoritePageReqVO.java |  1 +
 .../favorite/vo/ProductFavoriteRespVO.java    |  1 +
 .../favorite/ProductFavoriteConvert.java      | 23 ++++++++-----------
 .../favorite/ProductFavoriteDetailDO.java     | 15 ------------
 .../mysql/favorite/ProductFavoriteMapper.java |  9 ++++----
 7 files changed, 23 insertions(+), 46 deletions(-)
 delete mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java

diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index db054e972..57e7133fb 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -139,4 +139,5 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         // 转换返回
         return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
     }
+
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
index 6f66c2062..721cf19c0 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/ProductFavoriteController.java
@@ -41,20 +41,13 @@ public class ProductFavoriteController {
     @Operation(summary = "获得商品收藏分页")
     @PreAuthorize("@ss.hasPermission('product:favorite:query')")
     public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
-        PageResult<ProductFavoriteDO> favoritePage = productFavoriteService.getFavoritePage(pageVO);
-        if (CollUtil.isEmpty(favoritePage.getList())) {
+        PageResult<ProductFavoriteDO> pageResult = productFavoriteService.getFavoritePage(pageVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
             return success(PageResult.empty());
         }
-
-        List<ProductSpuDO> list = productSpuService.getSpuList(convertSet(favoritePage.getList(), ProductFavoriteDO::getSpuId));
-
-        // 得到商品 spu 信息
-        List<ProductFavoriteRespVO> favorites =  ProductFavoriteConvert.INSTANCE.convertList2admin(favoritePage.getList(), list);
-
-        // 转换 VO 结果
-        PageResult<ProductFavoriteRespVO> pageResult = new PageResult<>(favoritePage.getTotal());
-        pageResult.setList(favorites);
-
-        return success(pageResult);
+        // 拼接数据
+        List<ProductSpuDO> spuList = productSpuService.getSpuList(convertSet(pageResult.getList(), ProductFavoriteDO::getSpuId));
+        return success(ProductFavoriteConvert.INSTANCE.convertPage(pageResult, spuList));
     }
+
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
index 9c0b33035..3d78883ec 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoritePageReqVO.java
@@ -14,4 +14,5 @@ public class ProductFavoritePageReqVO extends PageParam {
 
     @Schema(description = "用户编号", example = "5036")
     private Long userId;
+
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
index bbf972181..3c09aa8fc 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/favorite/vo/ProductFavoriteRespVO.java
@@ -15,4 +15,5 @@ public class ProductFavoriteRespVO  extends ProductSpuRespVO {
 
     @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
     private Long spuId;
+
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
index 9adac8c86..7b419b6a8 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/favorite/ProductFavoriteConvert.java
@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.product.convert.favorite;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.product.controller.admin.favorite.vo.ProductFavoriteRespVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoriteRespVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDetailDO;
 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
@@ -13,7 +13,6 @@ import org.mapstruct.factory.Mappers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
@@ -38,20 +37,18 @@ public interface ProductFavoriteConvert {
         return resultList;
     }
 
+    default PageResult<ProductFavoriteRespVO> convertPage(PageResult<ProductFavoriteDO> pageResult, List<ProductSpuDO> spuList) {
+        Map<Long, ProductSpuDO> spuMap = convertMap(spuList, ProductSpuDO::getId);
+        List<ProductFavoriteRespVO> voList = CollectionUtils.convertList(pageResult.getList(), favorite -> {
+            ProductSpuDO spu = spuMap.get(favorite.getSpuId());
+            return convert02(spu, favorite);
+        });
+        return new PageResult<>(voList, pageResult.getTotal());
+    }
     @Mapping(target = "id", source = "favorite.id")
     @Mapping(target = "userId", source = "favorite.userId")
     @Mapping(target = "spuId", source = "favorite.spuId")
     @Mapping(target = "createTime", source = "favorite.createTime")
-    ProductFavoriteRespVO convert2admin(ProductSpuDO spu, ProductFavoriteDO favorite);
+    ProductFavoriteRespVO convert02(ProductSpuDO spu, ProductFavoriteDO favorite);
 
-    default List<ProductFavoriteRespVO> convertList2admin(List<ProductFavoriteDO> favorites, List<ProductSpuDO> spus) {
-        List<ProductFavoriteRespVO> resultList = new ArrayList<>(spus.size());
-        for (ProductFavoriteDO favorite : favorites) {
-            Optional<ProductSpuDO> spu =  spus.stream().filter(e -> e.getId().equals(favorite.getSpuId())).findFirst();
-            resultList.add(convert2admin(spu.get(), favorite));
-        }
-        return resultList;
-    }
-
-    PageResult<ProductFavoriteRespVO> convertPage(PageResult<ProductFavoriteDetailDO> page);
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
deleted file mode 100644
index 60e401e11..000000000
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/favorite/ProductFavoriteDetailDO.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package cn.iocoder.yudao.module.product.dal.dataobject.favorite;
-
-import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
-import lombok.Data;
-
-/**
- * 商品收藏 DO
- *
- * @author 芋道源码
- */
-@Data
-public class ProductFavoriteDetailDO extends ProductFavoriteDO {
-
-    ProductSpuDO spuDO;
-}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
index e116a7c4a..a681d42a7 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/favorite/ProductFavoriteMapper.java
@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.product.dal.mysql.favorite;
 
 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.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.controller.app.favorite.vo.AppFavoritePageReqVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.favorite.ProductFavoriteDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
@@ -24,10 +24,9 @@ public interface ProductFavoriteMapper extends BaseMapperX<ProductFavoriteDO> {
     }
 
     default PageResult<ProductFavoriteDO> selectPageByUserId(ProductFavoritePageReqVO reqVO) {
-        return selectPage(reqVO, new MPJLambdaWrapper<ProductFavoriteDO>()
-                .selectAll(ProductFavoriteDO.class)
-                .eq(ProductFavoriteDO::getUserId, reqVO.getUserId())
-                .orderByDesc(ProductFavoriteDO::getCreateTime));
+        return selectPage(reqVO, new LambdaQueryWrapperX<ProductFavoriteDO>()
+                .eqIfPresent(ProductFavoriteDO::getUserId, reqVO.getUserId())
+                .orderByDesc(ProductFavoriteDO::getId));
     }
 
     default Long selectCountByUserId(Long userId) {

From 7f75f0abfce424f98226f55bfb71ddb82a2d9a7c Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 19 Nov 2023 18:28:00 +0800
Subject: [PATCH 100/101] =?UTF-8?q?bpm=EF=BC=9Acode=20review=20=E6=8C=87?=
 =?UTF-8?q?=E5=AE=9A=E5=AE=A1=E6=89=B9=E4=BA=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../flowable/core/context/FlowableContextHolder.java   |  1 +
 .../api/task/dto/BpmProcessInstanceCreateReqDTO.java   |  7 ++++++-
 .../vo/instance/BpmProcessInstanceCreateReqVO.java     |  3 ++-
 .../dal/dataobject/task/BpmProcessInstanceExtDO.java   |  4 +++-
 .../definition/BpmTaskAssignRuleServiceImpl.java       | 10 ++++++----
 .../bpm/service/task/BpmProcessInstanceService.java    |  4 +++-
 .../service/task/BpmProcessInstanceServiceImpl.java    |  2 +-
 7 files changed, 22 insertions(+), 9 deletions(-)

diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
index e6021c0b7..efc6d5340 100644
--- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
+++ b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
@@ -36,4 +36,5 @@ public class FlowableContextHolder {
     public static void setAssignee(Map<String, List<Long>> assignee) {
         ASSIGNEE.set(assignee);
     }
+
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
index 6db999c1e..4403d3e68 100644
--- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
+++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
@@ -32,9 +32,14 @@ public class BpmProcessInstanceCreateReqDTO {
     @NotEmpty(message = "业务的唯一标识")
     private String businessKey;
 
+    // TODO @hai:assignees 复数
     /**
      * 提前指派的审批人
-     * 例如: { taskKey1 :[1,2] },则表示 taskKey1 这个任务,提前设定了,由 userId 为 1,2 的用户进行审批
+     *
+     * key:taskKey 任务编码
+     * value:审批人的数组
+     * 例如: { taskKey1 :[1, 2] },则表示 taskKey1 这个任务,提前设定了,由 userId 为 1,2 的用户进行审批
      */
     private Map<String, List<Long>> assignee;
+
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
index 13b7c8f0b..93cf541bb 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
@@ -18,7 +18,8 @@ public class BpmProcessInstanceCreateReqVO {
     @Schema(description = "变量实例")
     private Map<String, Object> variables;
 
-    @Schema(description = "提前指派的审批人", requiredMode = Schema.RequiredMode.REQUIRED, example = "{taskKey1:[1,2]}")
+    // TODO @hai:assignees 复数
+    @Schema(description = "提前指派的审批人", requiredMode = Schema.RequiredMode.REQUIRED, example = "{taskKey1: [1, 2]}")
     private Map<String, List<Long>> assignee;
 
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
index 8aba9059f..5a481fff0 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
@@ -88,9 +88,11 @@ public class BpmProcessInstanceExtDO extends BaseDO {
     @TableField(typeHandler = JacksonTypeHandler.class)
     private Map<String, Object> formVariables;
 
+    // TODO @hai:assignees 复数
     /**
      * 提前设定好的审批人
      */
-    @TableField(typeHandler = JacksonTypeHandler.class)
+    @TableField(typeHandler = JacksonTypeHandler.class, exist = false) // TODO 芋艿:临时 exist = false,避免 db 报错;
     private Map<String, List<Long>> assignee;
+
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
index 8c6ceaf48..9ef936376 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
@@ -239,12 +239,14 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
     @Override
     @DataPermission(enable = false) // 忽略数据权限,不然分配会存在问题
     public Set<Long> calculateTaskCandidateUsers(DelegateExecution execution) {
-        //1. 先从提前选好的审批人中获取
-        List<Long> assignee = processInstanceService.getAssigneeByProcessInstanceIdAndTaskDefinitionKey(execution.getProcessInstanceId(), execution.getCurrentActivityId());
-        if(CollUtil.isNotEmpty(assignee)){
+        // 1. 先从提前选好的审批人中获取
+        List<Long> assignee = processInstanceService.getAssigneeByProcessInstanceIdAndTaskDefinitionKey(
+                execution.getProcessInstanceId(), execution.getCurrentActivityId());
+        if (CollUtil.isNotEmpty(assignee)) {
+            // TODO @hai:new HashSet 即可
             return convertSet(assignee, Function.identity());
         }
-        //2. 通过分配规则,计算审批人
+        // 2. 通过分配规则,计算审批人
         BpmTaskAssignRuleDO rule = getTaskRule(execution);
         return calculateTaskCandidateUsers(execution, rule);
     }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
index 4649475db..cff0ec976 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
@@ -145,12 +145,14 @@ public interface BpmProcessInstanceService {
      */
     void updateProcessInstanceExtReject(String id, String reason);
 
+    // TODO @hai:改成 getProcessInstanceAssigneesByTaskDefinitionKey(String id, String taskDefinitionKey)
     /**
-     * 去流程实例扩展表中,取出指定流程任务提前指定的审批人
+     * 获取流程实例中,取出指定流程任务提前指定的审批人
      *
      * @param processInstanceId 流程实例的编号
      * @param taskDefinitionKey 流程任务定义的 key
      * @return 审批人集合
      */
     List<Long> getAssigneeByProcessInstanceIdAndTaskDefinitionKey(String processInstanceId, String taskDefinitionKey);
+
 }
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
index 5d5e1ba10..415913a9a 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -1 +1 @@
-package cn.iocoder.yudao.module.bpm.service.task;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.flowable.core.context.FlowableContextHolder;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.event.FlowableCancelledEvent;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.*;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;

/**
 * 流程实例 Service 实现类
 * <p>
 * ProcessDefinition & ProcessInstance & Execution & Task 的关系:
 * 1. <a href="https://blog.csdn.net/bobozai86/article/details/105210414" />
 * <p>
 * HistoricProcessInstance & ProcessInstance 的关系:
 * 1. <a href=" https://my.oschina.net/843294669/blog/71902" />
 * <p>
 * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
 *
 * @author 芋道源码
 */
@Service
@Validated
@Slf4j
public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService {

    @Resource
    private RuntimeService runtimeService;
    @Resource
    private BpmProcessInstanceExtMapper processInstanceExtMapper;
    @Resource
    @Lazy // 解决循环依赖
    private BpmTaskService taskService;
    @Resource
    private BpmProcessDefinitionService processDefinitionService;
    @Resource
    private HistoryService historyService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher;
    @Resource
    private BpmMessageService messageService;

    @Override
    public ProcessInstance getProcessInstance(String id) {
        return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
    }

    @Override
    public List<ProcessInstance> getProcessInstances(Set<String> ids) {
        return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
    }

    @Override
    public PageResult<BpmProcessInstancePageItemRespVO> getMyProcessInstancePage(Long userId,
                                                                                 BpmProcessInstanceMyPageReqVO pageReqVO) {
        // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页
        PageResult<BpmProcessInstanceExtDO> pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return new PageResult<>(pageResult.getTotal());
        }

        // 获得流程 Task Map
        List<String> processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId);
        Map<String, List<Task>> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds);
        // 转换返回
        return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId());
        // 发起流程
        return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getAssignee());
    }

    @Override
    public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey());
        // 发起流程
        return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getAssignee());
    }

    @Override
    public BpmProcessInstanceRespVO getProcessInstanceVO(String id) {
        // 获得流程实例
        HistoricProcessInstance processInstance = getHistoricProcessInstance(id);
        if (processInstance == null) {
            return null;
        }
        BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id);
        Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id);

        // 获得流程定义
        ProcessDefinition processDefinition = processDefinitionService
                .getProcessDefinition(processInstance.getProcessDefinitionId());
        Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId());
        BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt(
                processInstance.getProcessDefinitionId());
        Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id);
        String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId());

        // 获得 User
        AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId()));
        DeptRespDTO dept = null;
        if (startUser != null) {
            dept = deptApi.getDept(startUser.getDeptId());
        }

        // 拼接结果
        return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt,
                processDefinition, processDefinitionExt, bpmnXml, startUser, dept);
    }

    @Override
    public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) {
        // 校验流程实例存在
        ProcessInstance instance = getProcessInstance(cancelReqVO.getId());
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
        }
        // 只能取消自己的
        if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
        }

        // 通过删除流程实例,实现流程实例的取消,
        // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询
        deleteProcessInstance(cancelReqVO.getId(),
                BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason()));
    }

    /**
     * 获得历史的流程实例
     *
     * @param id 流程实例的编号
     * @return 历史的流程实例
     */
    @Override
    public HistoricProcessInstance getHistoricProcessInstance(String id) {
        return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult();
    }

    @Override
    public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) {
        return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
    }

    @Override
    public void createProcessInstanceExt(ProcessInstance instance) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId());
        // 插入 BpmProcessInstanceExtDO 对象
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(instance.getId())
                .setProcessDefinitionId(definition.getId())
                .setName(instance.getProcessDefinitionName())
                .setStartUserId(Long.valueOf(instance.getStartUserId()))
                .setCategory(definition.getCategory())
                .setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus())
                .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult());

        processInstanceExtMapper.insert(instanceExtDO);
    }

    @Override
    public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) {
        // 判断是否为 Reject 不通过。如果是,则不进行更新.
        // 因为,updateProcessInstanceExtReject 方法,已经进行更新了
        if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String) event.getCause())) {
            return;
        }

        // 需要主动查询,因为 instance 只有 id 属性
        // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
        HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId());
        // 更新拓展表
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(event.getProcessInstanceId())
                .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.CANCEL.getResult());
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    @Override
    public void updateProcessInstanceExtComplete(ProcessInstance instance) {
        // 需要主动查询,因为 instance 只有 id 属性
        // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
        HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId());
        // 更新拓展表
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(instance.getProcessInstanceId())
                .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程被通过的消息
        messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance));

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProcessInstanceExtReject(String id, String reason) {
        // 需要主动查询,因为 instance 只有 id 属性
        ProcessInstance processInstance = getProcessInstance(id);
        // 删除流程实例,以实现驳回任务时,取消整个审批流程
        deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason)));

        // 更新 status + result
        // 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法,
        // 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id)
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.REJECT.getResult());
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程被不通过的消息
        messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason));

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    private void deleteProcessInstance(String id, String reason) {
        runtimeService.deleteProcessInstance(id, reason);
    }

    private String createProcessInstance0(Long userId, ProcessDefinition definition,
                                          Map<String, Object> variables, String businessKey,
                                          Map<String, List<Long>> assignee) {
        // 校验流程定义
        if (definition == null) {
            throw exception(PROCESS_DEFINITION_NOT_EXISTS);
        }
        if (definition.isSuspended()) {
            throw exception(PROCESS_DEFINITION_IS_SUSPENDED);
        }
        //设置上下文信息
        FlowableContextHolder.setAssignee(assignee);

        // 创建流程实例
        ProcessInstance instance = runtimeService.createProcessInstanceBuilder()
                .processDefinitionId(definition.getId())
                .businessKey(businessKey)
                .name(definition.getName().trim())
                .variables(variables)
                .start();
        // 设置流程名字
        runtimeService.setProcessInstanceName(instance.getId(), definition.getName());

        // 补全流程实例的拓展表
        processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId())
                .setFormVariables(variables).setAssignee(assignee));
        return instance.getId();
    }

    @Override
    public List<Long> getAssigneeByProcessInstanceIdAndTaskDefinitionKey(String processInstanceId, String taskDefinitionKey) {
        //1. 先从上下文获取,首次提交数据库中查询不到
        List<Long> result = FlowableContextHolder.getAssigneeByTaskDefinitionKey(taskDefinitionKey);
        if (CollUtil.isNotEmpty(result)) {
            return result;
        }
        //2. 从数据库中获取
        BpmProcessInstanceExtDO instance = processInstanceExtMapper.selectByProcessInstanceId(processInstanceId);
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
        }
        if(CollUtil.isNotEmpty(instance.getAssignee())){
            return instance.getAssignee().get(taskDefinitionKey);
        }
        return Collections.emptyList();
    }

}
\ No newline at end of file
+package cn.iocoder.yudao.module.bpm.service.task;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.flowable.core.context.FlowableContextHolder;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.event.FlowableCancelledEvent;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.*;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;

/**
 * 流程实例 Service 实现类
 * <p>
 * ProcessDefinition & ProcessInstance & Execution & Task 的关系:
 * 1. <a href="https://blog.csdn.net/bobozai86/article/details/105210414" />
 * <p>
 * HistoricProcessInstance & ProcessInstance 的关系:
 * 1. <a href=" https://my.oschina.net/843294669/blog/71902" />
 * <p>
 * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
 *
 * @author 芋道源码
 */
@Service
@Validated
@Slf4j
public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService {

    @Resource
    private RuntimeService runtimeService;
    @Resource
    private BpmProcessInstanceExtMapper processInstanceExtMapper;
    @Resource
    @Lazy // 解决循环依赖
    private BpmTaskService taskService;
    @Resource
    private BpmProcessDefinitionService processDefinitionService;
    @Resource
    private HistoryService historyService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher;
    @Resource
    private BpmMessageService messageService;

    @Override
    public ProcessInstance getProcessInstance(String id) {
        return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
    }

    @Override
    public List<ProcessInstance> getProcessInstances(Set<String> ids) {
        return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
    }

    @Override
    public PageResult<BpmProcessInstancePageItemRespVO> getMyProcessInstancePage(Long userId,
                                                                                 BpmProcessInstanceMyPageReqVO pageReqVO) {
        // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页
        PageResult<BpmProcessInstanceExtDO> pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return new PageResult<>(pageResult.getTotal());
        }

        // 获得流程 Task Map
        List<String> processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId);
        Map<String, List<Task>> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds);
        // 转换返回
        return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId());
        // 发起流程
        return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getAssignee());
    }

    @Override
    public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey());
        // 发起流程
        return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getAssignee());
    }

    @Override
    public BpmProcessInstanceRespVO getProcessInstanceVO(String id) {
        // 获得流程实例
        HistoricProcessInstance processInstance = getHistoricProcessInstance(id);
        if (processInstance == null) {
            return null;
        }
        BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id);
        Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id);

        // 获得流程定义
        ProcessDefinition processDefinition = processDefinitionService
                .getProcessDefinition(processInstance.getProcessDefinitionId());
        Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId());
        BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt(
                processInstance.getProcessDefinitionId());
        Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id);
        String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId());

        // 获得 User
        AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId()));
        DeptRespDTO dept = null;
        if (startUser != null) {
            dept = deptApi.getDept(startUser.getDeptId());
        }

        // 拼接结果
        return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt,
                processDefinition, processDefinitionExt, bpmnXml, startUser, dept);
    }

    @Override
    public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) {
        // 校验流程实例存在
        ProcessInstance instance = getProcessInstance(cancelReqVO.getId());
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
        }
        // 只能取消自己的
        if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
        }

        // 通过删除流程实例,实现流程实例的取消,
        // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询
        deleteProcessInstance(cancelReqVO.getId(),
                BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason()));
    }

    /**
     * 获得历史的流程实例
     *
     * @param id 流程实例的编号
     * @return 历史的流程实例
     */
    @Override
    public HistoricProcessInstance getHistoricProcessInstance(String id) {
        return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult();
    }

    @Override
    public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) {
        return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
    }

    @Override
    public void createProcessInstanceExt(ProcessInstance instance) {
        // 获得流程定义
        ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId());
        // 插入 BpmProcessInstanceExtDO 对象
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(instance.getId())
                .setProcessDefinitionId(definition.getId())
                .setName(instance.getProcessDefinitionName())
                .setStartUserId(Long.valueOf(instance.getStartUserId()))
                .setCategory(definition.getCategory())
                .setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus())
                .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult());

        processInstanceExtMapper.insert(instanceExtDO);
    }

    @Override
    public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) {
        // 判断是否为 Reject 不通过。如果是,则不进行更新.
        // 因为,updateProcessInstanceExtReject 方法,已经进行更新了
        if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String) event.getCause())) {
            return;
        }

        // 需要主动查询,因为 instance 只有 id 属性
        // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
        HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId());
        // 更新拓展表
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(event.getProcessInstanceId())
                .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.CANCEL.getResult());
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    @Override
    public void updateProcessInstanceExtComplete(ProcessInstance instance) {
        // 需要主动查询,因为 instance 只有 id 属性
        // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance
        HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId());
        // 更新拓展表
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO()
                .setProcessInstanceId(instance.getProcessInstanceId())
                .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程被通过的消息
        messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance));

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProcessInstanceExtReject(String id, String reason) {
        // 需要主动查询,因为 instance 只有 id 属性
        ProcessInstance processInstance = getProcessInstance(id);
        // 删除流程实例,以实现驳回任务时,取消整个审批流程
        deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason)));

        // 更新 status + result
        // 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法,
        // 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的
        BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id)
                .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus())
                .setResult(BpmProcessInstanceResultEnum.REJECT.getResult());
        processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO);

        // 发送流程被不通过的消息
        messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason));

        // 发送流程实例的状态事件
        processInstanceResultEventPublisher.sendProcessInstanceResultEvent(
                BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult()));
    }

    private void deleteProcessInstance(String id, String reason) {
        runtimeService.deleteProcessInstance(id, reason);
    }

    private String createProcessInstance0(Long userId, ProcessDefinition definition,
                                          Map<String, Object> variables, String businessKey,
                                          Map<String, List<Long>> assignee) {
        // 校验流程定义
        if (definition == null) {
            throw exception(PROCESS_DEFINITION_NOT_EXISTS);
        }
        if (definition.isSuspended()) {
            throw exception(PROCESS_DEFINITION_IS_SUSPENDED);
        }
        // 设置上下文信息
        // TODO @hai:要不往 variables 存到一个全局固定 key 里,减少对上下文的依赖
        FlowableContextHolder.setAssignee(assignee);

        // 创建流程实例
        ProcessInstance instance = runtimeService.createProcessInstanceBuilder()
                .processDefinitionId(definition.getId())
                .businessKey(businessKey)
                .name(definition.getName().trim())
                .variables(variables)
                .start();
        // 设置流程名字
        runtimeService.setProcessInstanceName(instance.getId(), definition.getName());

        // 补全流程实例的拓展表
        processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId())
                .setFormVariables(variables).setAssignee(assignee));
        return instance.getId();
    }

    @Override
    public List<Long> getAssigneeByProcessInstanceIdAndTaskDefinitionKey(String processInstanceId, String taskDefinitionKey) {
        // 1. 先从上下文获取,首次提交数据库中查询不到
        List<Long> result = FlowableContextHolder.getAssigneeByTaskDefinitionKey(taskDefinitionKey);
        if (CollUtil.isNotEmpty(result)) {
            return result;
        }
        // 2. 从数据库中获取
        BpmProcessInstanceExtDO instance = processInstanceExtMapper.selectByProcessInstanceId(processInstanceId);
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
        }
        if (CollUtil.isNotEmpty(instance.getAssignee())) {
            return instance.getAssignee().get(taskDefinitionKey);
        }
        return Collections.emptyList();
    }

}
\ No newline at end of file

From 827897807febe62d7d661243d13f78b9d1e2deeb Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 19 Nov 2023 18:46:16 +0800
Subject: [PATCH 101/101] =?UTF-8?q?=E6=8F=90=E5=8D=87=E5=90=AF=E5=8A=A8?=
 =?UTF-8?q?=E9=80=9F=E5=BA=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 yudao-server/pom.xml | 60 ++++++++++++++++++++++----------------------
 1 file changed, 30 insertions(+), 30 deletions(-)

diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index fb50caa4f..20b67725b 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -37,11 +37,11 @@
         </dependency>
 
         <!-- 会员中心。默认注释,保证编译速度 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-member-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-member-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
 
         <!-- 数据报表。默认注释,保证编译速度 -->
 <!--        <dependency>-->
@@ -56,11 +56,11 @@
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
         <!-- 支付服务。默认注释,保证编译速度 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-pay-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-pay-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
 
         <!-- 微信公众号模块。默认注释,保证编译速度 -->
 <!--        <dependency>-->
@@ -70,26 +70,26 @@
 <!--        </dependency>-->
 
         <!-- 商城相关模块。默认注释,保证编译速度-->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-promotion-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-product-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-trade-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-module-statistics-biz</artifactId>
-            <version>${revision}</version>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-promotion-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-product-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-trade-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-statistics-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
+<!--        </dependency>-->
 
         <!-- CRM 相关模块。默认注释,保证编译速度 -->
 <!--        <dependency>-->