diff --git a/pom.xml b/pom.xml
index dae40f3dc..bcffebfd7 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/sql/mysql/crm.sql b/sql/mysql/crm.sql
new file mode 100644
index 000000000..e3e2ca0a0
--- /dev/null
+++ b/sql/mysql/crm.sql
@@ -0,0 +1 @@
+SET NAMES utf8mb4;
diff --git a/sql/mysql/crm_data.sql b/sql/mysql/crm_data.sql
new file mode 100644
index 000000000..b5be1e691
--- /dev/null
+++ b/sql/mysql/crm_data.sql
@@ -0,0 +1,20 @@
+
+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
new file mode 100644
index 000000000..a4b9105e0
--- /dev/null
+++ b/sql/mysql/crm_menu.sql
@@ -0,0 +1,88 @@
+-- ----------------------------
+-- 客户公海配置
+-- ----------------------------
+-- 菜单 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
+);
+
+
+
+
+-- ----------------------------
+-- 客户限制配置管理
+-- ----------------------------
+-- 菜单 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/sql/mysql/pay_wallet.sql b/sql/mysql/pay_wallet.sql
index 7c6c04003..1e9f1d255 100644
--- a/sql/mysql/pay_wallet.sql
+++ b/sql/mysql/pay_wallet.sql
@@ -246,3 +246,11 @@ VALUES (
            '转账订单', '', 2, 3, 1117,
            'transfer', 'ep:credit-card', 'pay/transfer/index', 0, 'PayTransfer'
        );
+
+-- 转账通知脚本
+
+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/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 '更新者',
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-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 919173da6..2d3232978 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
@@ -280,6 +280,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)) {
@@ -288,4 +297,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-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-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-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..efc6d5340
--- /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,40 @@
+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-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 0bc54d532..6a76024d5 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;
@@ -39,6 +40,13 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
     }
 
+    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
+        IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
+        selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
+        // 转换返回
+        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+    }
+
     default T selectOne(String field, Object value) {
         return selectOne(new QueryWrapper<T>().eq(field, value));
     }
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..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
@@ -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,15 @@ public class BpmProcessInstanceCreateReqDTO {
      */
     @NotEmpty(message = "业务的唯一标识")
     private String businessKey;
+
+    // TODO @hai:assignees 复数
+    /**
+     * 提前指派的审批人
+     *
+     * 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 8fc544ef4..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
@@ -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,8 @@ public class BpmProcessInstanceCreateReqVO {
     @Schema(description = "变量实例")
     private Map<String, Object> variables;
 
+    // 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 293cc2dd7..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
@@ -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,11 @@ public class BpmProcessInstanceExtDO extends BaseDO {
     @TableField(typeHandler = JacksonTypeHandler.class)
     private Map<String, Object> formVariables;
 
+    // TODO @hai:assignees 复数
+    /**
+     * 提前设定好的审批人
+     */
+    @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 c6200b2dd..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
@@ -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,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)) {
+            // TODO @hai:new HashSet 即可
+            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..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
@@ -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,19 @@ public interface BpmProcessInstanceService {
     /**
      * 更新 ProcessInstance 拓展记录为不通过
      *
-     * @param id 流程编号
+     * @param id     流程编号
      * @param reason 理由。例如说,审批不通过时,需要传递该值
      */
     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 56115d79b..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.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.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
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/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..85236fdd2
--- /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;
+
+// TODO @liuhongfeng:这个状态,还是搞成专属 CrmReceivableDO 专属的 status;
+/**
+ * 流程审批状态枚举类
+ * 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 已撤回;主要是留好间隙,万一每个地方要做点拓展; 然后,枚举字段的顺序调整下,审批中,一定要放两个审批通过、拒绝前面哈;
+    /**
+     * 未审批
+     */
+    AUDIT_NEW(0, "未审批"),
+    /**
+     * 审核通过
+     */
+	AUDIT_FINISH(1, "审核通过"),
+    /**
+     * 审核拒绝
+     */
+	AUDIT_REJECT(2, "审核拒绝"),
+    /**
+     * 审核中
+     */
+    AUDIT_DOING(3, "审核中"),
+	/**
+	 * 已撤回
+	 */
+	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();
+
+    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/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..71f550775
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+/**
+ * CRM 字典类型的枚举类
+ *
+ * @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 客户来源
+    String CRM_RECEIVABLE_CHECK_STATUS = "crm_receivable_check_status"; // CRM 审批状态
+
+}
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..b04644a41
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * CRM 错误码枚举类
+ * <p>
+ * crm 系统,使用 1-020-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+    // ========== 合同管理 1-020-000-000 ==========
+    ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
+
+    // ========== 线索管理 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, "商机不存在");
+
+    // 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, "商机状态不存在");
+
+    // ========== 联系人管理 1-020-003-000 ==========
+    ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
+
+    // ========== 回款管理 1-020-004-000 ==========
+    ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_020_004_000, "回款管理不存在");
+
+    // ========== 合同管理 1-020-005-000 ==========
+    ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_020_005_000, "回款计划不存在");
+
+    // ========== 客户管理 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_ALREADY_DEAL = new ErrorCode(1_020_006_003, "客户已交易");
+    // TODO @wanwan:这 2 个单独配置段噢
+    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, "数据权限不存在");
+    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_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-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..e6074c432
--- /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;
+
+// TODO @liuhongfeng:这个的作用是?
+/**
+ * @author 赤焰
+ */
+public enum ReturnTypeEnum {
+}
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..aa06b05eb
--- /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::getLevel).toArray();
+
+    /**
+     * 状态
+     */
+    private final Integer level;
+    /**
+     * 状态名
+     */
+    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/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..81cb674eb
--- /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,47 @@
+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;
+
+// TODO @puhui999:这个应该是 crm 全局的,不仅仅属于 customer 客户哈;
+/**
+ * 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/pom.xml b/yudao-module-crm/yudao-module-crm-biz/pom.xml
new file mode 100644
index 000000000..15bbc932d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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>
+        <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-module-crm-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <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>
+            <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>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</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/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..55adb4bd5
--- /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,32 @@
+### 请求 /transfer
+PUT {{baseUrl}}/crm/business/transfer
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "id": 1,
+  "ownerUserId": 2,
+  "transferType": 2,
+  "permissionType": 2
+}
+
+### 请求 /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/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..49c99991e
--- /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,107 @@
+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.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;
+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;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@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, getLoginUserId()));
+    }
+
+    @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("/page")
+    @Operation(summary = "获得商机分页")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPage(@Valid CrmBusinessPageReqVO pageVO) {
+        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO, getLoginUserId());
+        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')")
+    @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);
+    }
+
+    @PutMapping("/transfer")
+    @Operation(summary = "商机转移")
+    @PreAuthorize("@ss.hasPermission('crm:business:update')")
+    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/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/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..9bcffb117
--- /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,57 @@
+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 = "商机状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714")
+    @NotNull(message = "商机状态类型不能为空")
+    private Long statusTypeId;
+
+    @Schema(description = "商机状态编号", requiredMode = Schema.RequiredMode.REQUIRED, 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 Integer price;
+
+    // TODO @ljileo:折扣使用 Integer 类型,存储时,默认 * 100;展示的时候,前端需要 / 100;避免精度丢失问题
+    @Schema(description = "整单折扣")
+    private Integer discountPercent;
+
+    @Schema(description = "产品总金额", example = "12025")
+    private BigDecimal productPrice;
+
+    @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/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..f743c8469
--- /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,16 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.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 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/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..e7e3ef987
--- /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,75 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Set;
+
+/**
+ * 商机 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 Set<Long> roUserIds;
+
+    @ExcelProperty("读写权限的用户编号数组")
+    private Set<Long> 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..c8368cce7
--- /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,18 @@
+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;
+
+@Schema(description = "管理后台 - 商机分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessPageReqVO extends PageParam {
+
+    @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/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/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..b5a1153f7
--- /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,32 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.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 = "管理后台 - 商机转移 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 newOwnerUserId;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer oldOwnerPermissionLevel;
+
+}
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..f137d4c5b
--- /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,22 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.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 CrmBusinessUpdateReqVO extends CrmBusinessBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    @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/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..275285098
--- /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,119 @@
+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;
+
+// TODO @lilleo:这个模块,可以挪到 business 下;这样我打开 business 包下,就知道,噢~原来里面有 business 商机、有 type 状态类型、status 具体状态;
+@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));
+    }
+
+    // TODO @lilleo:这个接口,暂时用不到,可以考虑先删除掉
+    @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);
+    }
+
+    // TODO 芋艿:后续再看看
+    @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));
+    }
+
+    // TODO 芋艿:后续再看看
+    @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..401e35fbe
--- /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,33 @@
+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 {
+
+    // TODO @lilleo:example 要写下
+
+    @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;
+
+    // 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/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..78da092f7
--- /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,30 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
+/**
+ * 商机状态 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..7f7fba6c7
--- /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,23 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
+@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..54f675272
--- /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..429902164
--- /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,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.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 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..25ba7448f
--- /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,110 @@
+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;
+
+// TODO @lilleo:这个模块,可以挪到 business 下;这样我打开 business 包下,就知道,噢~原来里面有 business 商机、有 type 状态类型、status 具体状态;
+@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));
+    }
+
+    // TODO @lilleo:这个接口,暂时用不到,可以考虑先删除掉
+    @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..5000e25ee
--- /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,15 @@
+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;
+
+// TODO 状态类型和状态添加,是在一个请求里,所以需要把 CrmBusinessStatusCreateReqVO 融合进来;
+@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..cc6ed8502
--- /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,32 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
+/**
+ * 商机状态类型 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..1345565be
--- /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 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;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
+@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..0eb93224c
--- /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,21 @@
+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;
+
+import javax.validation.constraints.NotNull;
+
+// TODO 状态类型和状态添加,是在一个请求里,所以需要把 CrmBusinessStatusUpdateReqVO 融合进来;
+@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/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..8892a8e71
--- /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,89 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue;
+
+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.clue.vo.*;
+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
+@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("/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/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/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..f8ca48444
--- /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,52 @@
+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;
+
+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 = "线索xxx")
+    @NotEmpty(message = "线索名称不能为空")
+    private String name;
+
+    @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;
+
+    @Schema(description = "电话", example = "18000000000")
+    @Telephone
+    private String telephone;
+
+    @Schema(description = "手机号", example = "18000000000")
+    @Mobile
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @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..d6457bd56
--- /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,66 @@
+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.*;
+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(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean transformStatus;
+
+    @ExcelProperty(value = "跟进状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean followUpStatus;
+
+    @ExcelProperty("线索名称")
+    private String name;
+
+    // TODO 这里需要导出成客户名称
+    @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..4d28ebc73
--- /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,24 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.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 CrmCluePageReqVO extends PageParam {
+
+    @Schema(description = "线索名称", example = "线索xxx")
+    private String name;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @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/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..6d2d30334
--- /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,27 @@
+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")
+@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;
+
+    @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/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..4526fbd2b
--- /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,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.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 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/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..bf376ffa3
--- /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,135 @@
+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 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 java.util.Map;
+import java.util.Optional;
+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 联系人")
+@RestController
+@RequestMapping("/crm/contact")
+@Validated
+public class ContactController {
+
+    @Resource
+    private ContactService contactService;
+    // TODO @zyna:模块内,注入的变量,不用带 crm 前缀哈
+    @Resource
+    private CrmCustomerService crmCustomerService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建联系人")
+    @PreAuthorize("@ss.hasPermission('crm:contact:create')")
+    public CommonResult<Long> createContact(@Valid @RequestBody ContactCreateReqVO createReqVO) {
+        return success(contactService.createContact(createReqVO, getLoginUserId()));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新联系人")
+    @PreAuthorize("@ss.hasPermission('crm:contact:update')")
+    public CommonResult<Boolean> updateContact(@Valid @RequestBody ContactUpdateReqVO updateReqVO) {
+        contactService.updateContact(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除联系人")
+    @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 = "获得联系人")
+    @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));
+        contactRespVO.setCustomerName(Optional.ofNullable(crmCustomerService.getCustomer(contact.getCustomerId())).map(CrmCustomerDO::getName).orElse(null));
+        return success(contactRespVO);
+    }
+
+    // TODO @zyna:url 使用中划线噢;然后,单词的拼写也要注意呀,AllList 是不是更好呀;
+    @GetMapping("/simpleAlllist")
+    @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 = "获得联系人分页")
+    @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));
+        pageResult.getList().forEach(item -> {
+            item.setCustomerName(Optional.ofNullable(crmCustomerDOMap.get(item.getCustomerId())).map(CrmCustomerDO::getName).orElse(null));
+        });
+        return success(pageResult);
+    }
+
+    // TODO @zyna:可以看下新的导出写法,这里调整下
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出联系人 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..9311ad365
--- /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,73 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.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;
+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 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ContactBaseVO {
+
+    // TODO @zyna:example 最好都写下
+    // TODO @zyna:必要的字段校验,例如说 @Mobile,@Emal 等等
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+    private LocalDateTime nextTime;
+
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "电话")
+    private String telephone;
+
+    @Schema(description = "电子邮箱")
+    private String email;
+
+    @Schema(description = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @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
new file mode 100644
index 000000000..5eccfea74
--- /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 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 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..d13db6c3e
--- /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,72 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+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("下次联系时间")
+    private LocalDateTime nextTime;
+
+    @ExcelProperty("手机号")
+    private String mobile;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("电子邮箱")
+    private String email;
+
+    @ExcelProperty("客户编号")
+    private Long customerId;
+
+    @ExcelProperty("地址")
+    private String address;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @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
new file mode 100644
index 000000000..f05f6dcde
--- /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,71 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.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;
+
+// TODO @zyna:参考新的 VO 结构,使用 ContactPageReqVO 查询导出的数据
+@Schema(description = "管理后台 - crm联系人 Excel 导出 Request VO,参数和 ContactPageReqVO 是一致的")
+@Data
+@Deprecated
+public class ContactExportReqVO {
+
+    @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 = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @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;
+
+    @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
new file mode 100644
index 000000000..a20826b0a
--- /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,79 @@
+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 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 {
+
+    // TODO @zyna:筛选条件
+    // ●客户:
+    // ●姓名:
+    // ●手机、电话、座机、QQ、微信、邮箱
+
+    @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 = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @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;
+
+    @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
new file mode 100644
index 000000000..5a69424dc
--- /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,27 @@
+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 = "主键", 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
new file mode 100644
index 000000000..98d7da034
--- /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,17 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 联系人 Response VO")
+@Data
+@ToString(callSuper = true)
+public class ContactSimpleRespVO {
+    @Schema(description = "姓名", example = "芋艿") // TODO @zyna:requiredMode = Schema.RequiredMode.REQUIRED;需要空一行;字段的顺序改下,id 在 name 前面,会更干净
+    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
new file mode 100644
index 000000000..809009b0e
--- /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,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 javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 联系人更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactUpdateReqVO extends ContactBaseVO {
+
+    @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/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..2acc26a97
--- /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,32 @@
+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;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer oldOwnerPermissionLevel;
+
+}
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..1028929db
--- /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,98 @@
+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.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
+@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, getLoginUserId()));
+    }
+
+    @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("/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);
+    }
+
+    @PutMapping("/transfer")
+    @Operation(summary = "合同转移")
+    @PreAuthorize("@ss.hasPermission('crm:contract:update')")
+    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/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..756ee0d1f
--- /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,82 @@
+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;
+
+// TODO @dhb52:所有类,带下 Crm 前缀,避免和别的模块重复
+/**
+ * 合同 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@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;
+
+    @Schema(description = "商机编号", example = "10864")
+    private Long businessId;
+
+    @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;
+
+    @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;
+
+    // TODO @dhb52:增加一个 status 字段:具体有哪些值,你来枚举下;主要页面上有个【草稿】【提交审核】的流程,可以看看。然后要对接工作流,这块也可以看看,不确定的地方问我。
+
+}
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..7793d7737
--- /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,14 @@
+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 = "管理后台 - CRM 合同创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContractCreateReqVO extends ContractBaseVO {
+
+}
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..2fb521321
--- /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;
+
+/**
+ * CRM 合同 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..003e1f57c
--- /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 = "管理后台 - CRM 合同 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..36c7e14be
--- /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 = "管理后台 - CRM 合同分页 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..4a22251b0
--- /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 = "管理后台 - CRM 合同 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..34a9797f4
--- /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,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;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 合同更新 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;
+
+}
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..4ebef5943
--- /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,32 @@
+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;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer oldOwnerPermissionLevel;
+
+}
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..8a34b599d
--- /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,210 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer;
+
+import cn.hutool.core.collection.CollUtil;
+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;
+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 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.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+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.error;
+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;
+
+@Tag(name = "管理后台 - CRM 客户")
+@RestController
+@RequestMapping("/crm/customer")
+@Validated
+public class CrmCustomerController {
+
+    @Resource
+    private CrmCustomerService customerService;
+
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private CrmPermissionService permissionService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建客户")
+    @PreAuthorize("@ss.hasPermission('crm:customer:create')")
+    public CommonResult<Long> createCustomer(@Valid @RequestBody CrmCustomerCreateReqVO createReqVO) {
+        return success(customerService.createCustomer(createReqVO, getLoginUserId()));
+    }
+
+    @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) {
+        // 1. 获取客户
+        CrmCustomerDO customer = customerService.getCustomer(id);
+        if (customer == null) {
+            return success(null);
+        }
+
+        // 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));
+    }
+
+    // TODO @puhui999:可以在 CrmCustomerPageReqVO 里面加个 pool 参数,为 true 时,代表来自公海客户的分页
+    @GetMapping("/page")
+    @Operation(summary = "获得客户分页")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
+        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO, getLoginUserId());
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty(pageResult.getTotal()));
+        }
+        // 拼接数据
+        // 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(
+//                convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        return convertPage(customerService.getCustomerPage(pageVO, getLoginUserId()));
+    }
+
+    // TODO @puhui999:
+    @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")
+    @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);
+    }
+
+    @PutMapping("/transfer")
+    @Operation(summary = "客户转移")
+    @PreAuthorize("@ss.hasPermission('crm:customer:update')")
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmCustomerTransferReqVO reqVO) {
+        customerService.transferCustomer(reqVO, getLoginUserId());
+        return success(true);
+    }
+
+    // TODO @Joey:单独建一个属于自己业务的 ReqVO;因为前端如果模拟请求,是不是可以更新其它字段了;
+    @PutMapping("/lock")
+    @Operation(summary = "锁定/解锁客户")
+    @PreAuthorize("@ss.hasPermission('crm:customer:update')")
+    public CommonResult<Boolean> lockCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
+        customerService.lockCustomer(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/receive")
+    @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){
+        // 判断是否为空
+        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')")
+    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());
+        customerService.distributeByIds(cIds,ownerId);
+        return success("分配成功");
+    }
+
+}
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..b4d4751fc
--- /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,98 @@
+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.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.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;
+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.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+
+@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(customerLimitConfig.getUserIds());
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(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);
+        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
new file mode 100644
index 000000000..6bd1da362
--- /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,46 @@
+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));
+    }
+
+    // TODO @wanwan:这个请求,搞成 save 哈;
+    @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/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..c654b4b56
--- /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,80 @@
+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;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+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;
+
+/**
+ * 客户 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmCustomerBaseVO {
+
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
+    @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;
+
+    @Schema(description = "电话", example = "18000000000")
+    @Telephone
+    private String telephone;
+
+    @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 = "客户描述长度不能超过 4096 个字符")
+    private String description;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "地区编号", example = "20158")
+    private Integer areaId;
+
+    @Schema(description = "详细地址", example = "北京市海淀区")
+    private String detailAddress;
+
+    @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..41324079d
--- /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;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 客户创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerCreateReqVO extends CrmCustomerBaseVO {
+
+    @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
new file mode 100644
index 000000000..d49f569b3
--- /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,93 @@
+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 io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+// TODO 芋艿:导出最后做,等基本确认的差不多之后;
+/**
+ * CRM 客户 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(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;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @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;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("地区编号")
+    private Integer areaId;
+
+    @ExcelProperty("详细地址")
+    private String detailAddress;
+
+    @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..3a37c2834
--- /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,17 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+// TODO 芋艿:导出最后做,等基本确认的差不多之后;
+@Schema(description = "管理后台 - CRM 客户 Excel 导出 Request VO,参数和 CrmCustomerPageReqVO 是一致的")
+@Data
+public class CrmCustomerExportReqVO {
+
+    @Schema(description = "客户名称", example = "赵六")
+    private String name;
+
+    @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/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..7b163913e
--- /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,34 @@
+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;
+
+// TODO @wanwan:vo 下,可以新建一个 limitconfig,放它的 vo;
+/**
+ * 客户限制配置 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/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..fafe3d770
--- /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,39 @@
+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;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 客户分页 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 = "1")
+    private Integer industryId;
+
+    @Schema(description = "客户等级", example = "1")
+    private Integer level;
+
+    @Schema(description = "客户来源", example = "1")
+    private Integer source;
+
+    /**
+     * 场景类型
+     *
+     * 关联 {@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/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..0f6b6f33d
--- /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,32 @@
+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 {
+
+    // TODO @wanwan:参数校验
+    @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..c5af3cad2
--- /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,15 @@
+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;
+
+// TODO @wanwan:vo 下,可以新建一个 poolconfig,放它的 vo;
+@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/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..2cbd85dd3
--- /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,56 @@
+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 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 客户 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 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 = "负责人名字", example = "25682")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    private String ownerUserDeptName;
+
+    @Schema(description = "地区名称", example = "北京市")
+    private String areaName;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
+    @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/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..ed7cfb5c5
--- /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,32 @@
+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;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    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/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..6ed1566b3
--- /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,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 = "管理后台 - CRM 客户更新 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;
+
+}
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
new file mode 100644
index 000000000..75e8ad658
--- /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,167 @@
+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.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;
+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.*;
+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
+@RequestMapping("/crm/permission")
+@Validated
+public class CrmPermissionController {
+
+    @Resource
+    private CrmPermissionService permissionService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
+    @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)
+    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')")
+    public CommonResult<Boolean> receive(@RequestParam("bizType") Integer bizType, @RequestParam("bizId") Long bizId) {
+        permissionService.receiveBiz(bizType, bizId, getLoginUserId());
+        return success(true);
+    }
+
+    // TODO @puhui999:是不是放到客户那更合适哈?
+    @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 = "编辑团队成员权限")
+    @PreAuthorize("@ss.hasPermission('crm:permission:update')")
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_PERMISSION, bizTypeValue = "#updateReqVO.bizType", bizId = "#updateReqVO.bizId"
+            , level = CrmPermissionLevelEnum.OWNER)
+    public CommonResult<Boolean> updatePermission(@Valid @RequestBody CrmPermissionUpdateReqVO updateReqVO) {
+        permissionService.updatePermission(updateReqVO);
+        return success(true);
+    }
+
+    // TODO @puhui999:bizType 和 bizId 是不是不用啦;因为参数校验需要 bizType 和 bizId,可以先查询下,在直接调用方法;不一定都要注解哈;
+    @DeleteMapping("/delete")
+    @Operation(summary = "移除团队成员")
+    @Parameters({
+            @Parameter(name = "bizType", description = "CRM 类型", required = true, example = "2"),
+            @Parameter(name = "bizId", description = "CRM 类型数据编号", 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) // 为了校验权限请求必须带上 bizType 和  bizId
+    public CommonResult<Boolean> deletePermission(@RequestParam("bizType") Integer bizType,
+                                                  @RequestParam("bizId") Long bizId,
+                                                  @RequestParam("ids") Collection<Long> ids) {
+        permissionService.deletePermission(ids);
+        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')")
+    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);
+    }
+
+    @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 = permissionService.getPermissionByBizTypeAndBizId(bizType, bizId);
+        if (CollUtil.isEmpty(permission)) {
+            return success(Collections.emptyList());
+        }
+        // TODO @puhui999:池子的逻辑;
+        // 判断是否是公海数据
+        // TODO @puhui999:这段逻辑,可以删除么?
+        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));
+        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
new file mode 100644
index 000000000..595b8f9f2
--- /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 level;
+
+}
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..9440a949f
--- /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,28 @@
+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")
+@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 nickname;
+
+    @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部")
+    private String deptName;
+
+    @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
new file mode 100644
index 000000000..a6f0b1133
--- /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,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 javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - CRM 数据权限更新 Request VO")
+@Data
+public class CrmPermissionUpdateReqVO {
+
+    @Schema(description = "数据权限编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2]")
+    @NotNull(message = "数据权限编号列表不能为空")
+    private List<Long> ids;
+
+    @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/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..34c832f82
--- /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,89 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product;
+
+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.product.vo.*;
+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
+@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("/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/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/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..15718f4c5
--- /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,49 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+// TODO @zange:需要加 CRM 前置噢
+/**
+ * 产品 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ProductBaseVO {
+
+    // TODO @zange:example 要写哈;主要是接口文档,可以基于 example 可以生产请求参数
+
+    @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;
+
+    // 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/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..b4d46873e
--- /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,50 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.vo;
+
+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
+ *
+ * @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..ead2df93f
--- /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 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;
+
+// TODO 芋艿:这个导出最后搞
+@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..db2d3f94e
--- /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,49 @@
+package cn.iocoder.yudao.module.crm.controller.admin.product.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;
+
+// TODO @zange:按照需求,裁剪下筛选的字段,目前应该只要 name 和 status
+@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..9681520a4
--- /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,23 @@
+package cn.iocoder.yudao.module.crm.controller.admin.productcategory.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 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..e74df4a0c
--- /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,23 @@
+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;
+
+// TODO 芋艿:这个导出最后搞;命名应该是按照 ProductExportReqVO 风格
+@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..82575fbb1
--- /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,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.productcategory.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 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/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
new file mode 100644
index 000000000..490071900
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
@@ -0,0 +1,90 @@
+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.*;
+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;
+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 = "管理后台 - CRM 回款管理")
+@RestController
+@RequestMapping("/crm/receivable")
+@Validated
+public class CrmReceivableController {
+
+    // TODO @liuhongfeng:crmReceivableService 可以使用 receivableService ;在自己模块里,相对简洁一点;
+    @Resource
+    private CrmReceivableService crmReceivableService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建回款管理")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:create')")
+    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 CrmReceivableUpdateReqVO updateReqVO) {
+        crmReceivableService.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) {
+        crmReceivableService.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<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<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 CrmReceivableExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmReceivableDO> list = crmReceivableService.getReceivableList(exportReqVO);
+        // 导出 Excel
+        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/CrmReceivablePlanController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
new file mode 100644
index 000000000..afba2b721
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
@@ -0,0 +1,89 @@
+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.*;
+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;
+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 = "管理后台 - CRM 回款计划")
+@RestController
+@RequestMapping("/crm/receivable-plan")
+@Validated
+public class CrmReceivablePlanController {
+
+    @Resource
+    private CrmReceivablePlanService crmReceivablePlanService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建回款计划")
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:create')")
+    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 CrmReceivablePlanUpdateReqVO updateReqVO) {
+        crmReceivablePlanService.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) {
+        crmReceivablePlanService.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<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<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 CrmReceivablePlanExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmReceivablePlanDO> list = crmReceivablePlanService.getReceivablePlanList(exportReqVO);
+        // 导出 Excel
+        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/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/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
new file mode 100644
index 000000000..7b3ffd881
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableBaseVO.java
@@ -0,0 +1,67 @@
+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.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 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;
+
+    @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 Integer price;
+
+    // TODO @liuhongfeng:负责人编号
+    @Schema(description = "负责人", example = "22202")
+    private Long ownerUserId;
+
+    @Schema(description = "批次", example = "2539")
+    private Long batchId;
+
+    @Schema(description = "显示顺序")
+    private Integer sort;
+
+    @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/CrmReceivableCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableCreateReqVO.java
new file mode 100644
index 000000000..a2d43d8c1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableCreateReqVO.java
@@ -0,0 +1,12 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import lombok.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "管理后台 - CRM 回款创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+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/CrmReceivableExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java
new file mode 100644
index 000000000..291431e2c
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExcelVO.java
@@ -0,0 +1,62 @@
+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 com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+// TODO liuhongfeng:导出可以等其它功能做完,统一在搞;
+@Data
+public class CrmReceivableExcelVO {
+
+    @ExcelProperty("ID")
+    private Long id;
+
+    @ExcelProperty("回款编号")
+    private String no;
+
+    @ExcelProperty("回款计划ID")
+    private Long planId;
+
+    @ExcelProperty("客户名称")
+    private Long customerId;
+
+    @ExcelProperty("合同名称")
+    private Long contractId;
+
+    @ExcelProperty(value = "审批状态", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_RECEIVABLE_CHECK_STATUS)
+    private Integer checkStatus;
+
+    @ExcelProperty("工作流编号")
+    private Long processInstanceId;
+
+    @ExcelProperty("回款日期")
+    private LocalDateTime returnTime;
+
+    @ExcelProperty("回款方式")
+    private String returnType;
+
+    @ExcelProperty("回款金额")
+    private Integer price;
+
+    @ExcelProperty("负责人")
+    private Long ownerUserId;
+
+    @ExcelProperty("批次")
+    private Long batchId;
+
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.COMMON_STATUS)
+    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/CrmReceivableExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java
new file mode 100644
index 000000000..c8bdb16e7
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableExportReqVO.java
@@ -0,0 +1,57 @@
+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.time.LocalDateTime;
+
+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,参数和 CrmReceivablePageReqVO 是一致的")
+@Data
+public class CrmReceivableExportReqVO {
+
+    @Schema(description = "回款编号")
+    private String no;
+
+    @Schema(description = "回款计划", example = "31177")
+    private Long planId;
+
+    @Schema(description = "客户名称", example = "4963")
+    private Long customerId;
+
+    @Schema(description = "合同名称", example = "30305")
+    private Long contractId;
+
+    @Schema(description = "审批状态", example = "1")
+    private Integer checkStatus;
+
+    @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 Integer price;
+
+    @Schema(description = "负责人", example = "22202")
+    private Long ownerUserId;
+
+    @Schema(description = "批次", example = "2539")
+    private Long batchId;
+
+    @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/CrmReceivablePageReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java
new file mode 100644
index 000000000..61bd6f9a5
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePageReqVO.java
@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.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 = "管理后台 - CRM 回款分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmReceivablePageReqVO extends PageParam {
+
+    // TODO @liuhongfeng:可以根据需求,去除掉一些不要的过滤条件;另外,planId、customerId、contractId、ownerUserId 注释不正确,应该都是对应的编号
+
+    @Schema(description = "回款编号")
+    private String no;
+
+    @Schema(description = "回款计划ID", example = "31177")
+    private Long planId;
+
+    @Schema(description = "客户名称", example = "4963")
+    private Long customerId;
+
+    @Schema(description = "合同名称", example = "30305")
+    private Long contractId;
+
+    @Schema(description = "审批状态", example = "1")
+    private Integer checkStatus;
+
+    @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 Integer price;
+
+    @Schema(description = "负责人", example = "22202")
+    private Long ownerUserId;
+
+
+    @Schema(description = "状态", example = "1")
+    private Integer 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/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
new file mode 100644
index 000000000..eaba43dce
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanBaseVO.java
@@ -0,0 +1,66 @@
+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.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 CrmReceivablePlanBaseVO {
+
+    @Schema(description = "期数", example = "1")
+    private Integer period;
+
+    // TODO @liuhongfeng:回款计划编号
+    @Schema(description = "回款计划", example = "19852")
+    private Long receivableId;
+
+    @Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer status;
+
+    @Schema(description = "审批状态", example = "1")
+    @InEnum(AuditStatusEnum.class)
+    private Integer checkStatus;
+
+    @Schema(description = "计划回款金额", example = "29675")
+    private Integer price;
+
+    @Schema(description = "计划回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime returnTime;
+
+    @Schema(description = "提前几天提醒")
+    private Integer remindDays;
+
+    @Schema(description = "提醒日期")
+    @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;
+
+    @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/CrmReceivablePlanCreateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanCreateReqVO.java
new file mode 100644
index 000000000..cebfd28cb
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanCreateReqVO.java
@@ -0,0 +1,12 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo;
+
+import lombok.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "管理后台 - CRM 回款计划创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+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/CrmReceivablePlanExcelVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java
new file mode 100644
index 000000000..f4dd28366
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExcelVO.java
@@ -0,0 +1,70 @@
+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 com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+// TODO liuhongfeng:导出可以等其它功能做完,统一在搞;
+/**
+ * CRM 回款计划 Excel VO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class CrmReceivablePlanExcelVO {
+
+    @ExcelProperty("ID")
+    private Long id;
+
+    @ExcelProperty("期数")
+    private Integer period;
+
+    @ExcelProperty("回款ID")
+    private Long receivableId;
+
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.COMMON_STATUS)
+    private Integer status;
+
+    @ExcelProperty(value = "审批状态", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_RECEIVABLE_CHECK_STATUS)
+    private Integer checkStatus;
+
+    //@ExcelProperty("工作流编号")
+    //private Long processInstanceId;
+
+    @ExcelProperty("计划回款金额")
+    private Integer price;
+
+    @ExcelProperty("计划回款日期")
+    private LocalDateTime returnTime;
+
+    @ExcelProperty("提前几天提醒")
+    private Integer remindDays;
+
+    @ExcelProperty("提醒日期")
+    private LocalDateTime remindTime;
+
+    @ExcelProperty("客户ID")
+    private Long customerId;
+
+    @ExcelProperty("合同名称")
+    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/CrmReceivablePlanExportReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java
new file mode 100644
index 000000000..cbe86bd05
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanExportReqVO.java
@@ -0,0 +1,52 @@
+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.time.LocalDateTime;
+
+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 {
+
+    @Schema(description = "期数")
+    private Integer period;
+
+    @Schema(description = "完成状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "审批状态", example = "1")
+    private Integer checkStatus;
+
+    @Schema(description = "计划回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] returnTime;
+
+    @Schema(description = "提前几天提醒")
+    private Integer remindDays;
+
+    @Schema(description = "提醒日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] remindTime;
+
+    @Schema(description = "客户名称", example = "18026")
+    private Long customerId;
+
+    @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/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
new file mode 100644
index 000000000..a510753dd
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanPageReqVO.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.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 = "管理后台 - CRM 回款计划分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmReceivablePlanPageReqVO extends PageParam {
+
+    // TODO @liuhongfeng:可以根据需求,去除掉一些不要的过滤条件;另外,customerId、contractId、ownerUserId 注释不正确,应该都是对应的编号
+
+    @Schema(description = "完成状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "审批状态", example = "1")
+    private Integer checkStatus;
+
+    @Schema(description = "计划回款日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] returnTime;
+
+    @Schema(description = "提醒日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] remindTime;
+
+    @Schema(description = "客户名称", example = "18026")
+    private Long customerId;
+
+    @Schema(description = "合同名称", example = "3473")
+    private Long contractId;
+
+    @Schema(description = "负责人", example = "17828")
+    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/receivable/vo/CrmReceivablePlanRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanRespVO.java
new file mode 100644
index 000000000..243e7f782
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanRespVO.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 = "管理后台 - CRM 回款计划 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmReceivablePlanRespVO extends CrmReceivablePlanBaseVO {
+
+    @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/CrmReceivablePlanUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanUpdateReqVO.java
new file mode 100644
index 000000000..5471cfba3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivablePlanUpdateReqVO.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 javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - CRM 回款计划更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmReceivablePlanUpdateReqVO extends CrmReceivablePlanBaseVO {
+
+    @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/controller/admin/receivable/vo/CrmReceivableRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableRespVO.java
new file mode 100644
index 000000000..00d984da5
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableRespVO.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 = "管理后台 - CRM 回款 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmReceivableRespVO extends CrmReceivableBaseVO {
+
+    @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/CrmReceivableUpdateReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableUpdateReqVO.java
new file mode 100644
index 000000000..d6241f258
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/CrmReceivableUpdateReqVO.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 javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - CRM 回款更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmReceivableUpdateReqVO extends CrmReceivableBaseVO {
+
+    @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/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/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..8b7f7e83b
--- /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,40 @@
+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.CrmPermissionTransferReqBO;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 商机 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);
+
+    PageResult<CrmBusinessRespVO> convertPage(PageResult<CrmBusinessDO> page);
+
+    List<CrmBusinessExcelVO> convertList02(List<CrmBusinessDO> list);
+
+    @Mappings({
+            @Mapping(target = "bizId", source = "reqVO.id"),
+            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
+    })
+    CrmPermissionTransferReqBO convert(CrmBusinessTransferReqVO reqVO, Long userId);
+
+}
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/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..76ea428c7
--- /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,32 @@
+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);
+
+    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/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..a5be7d84f
--- /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,44 @@
+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.CrmPermissionTransferReqBO;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 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);
+
+    List<ContactSimpleRespVO> convertAllList(List<ContactDO> list);
+
+    @Mappings({
+            @Mapping(target = "bizId", source = "reqVO.id"),
+            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
+    })
+    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
new file mode 100644
index 000000000..09c61dd6d
--- /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,42 @@
+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.CrmPermissionTransferReqBO;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+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);
+
+    @Mappings({
+            @Mapping(target = "bizId", source = "reqVO.id"),
+            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
+    })
+    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
new file mode 100644
index 000000000..732a59f84
--- /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,110 @@
+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.dal.dataobject.customer.CrmCustomerPoolConfigDO;
+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
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmCustomerConvert {
+
+    CrmCustomerConvert INSTANCE = Mappers.getMapper(CrmCustomerConvert.class);
+
+    CrmCustomerDO convert(CrmCustomerCreateReqVO bean);
+
+    CrmCustomerDO convert(CrmCustomerUpdateReqVO bean);
+
+    CrmCustomerRespVO convert(CrmCustomerDO bean);
+
+    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;
+    }
+
+    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.setOwnerUserDeptName(dept.getName()));
+            });
+        });
+        return result;
+    }
+
+    List<CrmCustomerExcelVO> convertList02(List<CrmCustomerDO> list);
+
+    @Mappings({
+            @Mapping(target = "bizId", source = "reqVO.id"),
+            @Mapping(target = "newOwnerUserId", source = "reqVO.id")
+    })
+    CrmPermissionTransferReqBO convert(CrmCustomerTransferReqVO reqVO, Long userId);
+
+    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);
+        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;
+    }
+
+    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/customer/CrmCustomerLimitConfigConvert.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerLimitConfigConvert.java
new file mode 100644
index 000000000..33b0cae20
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerLimitConfigConvert.java
@@ -0,0 +1,67 @@
+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;
+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) {
+        // 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/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/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..42f784ba7
--- /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,67 @@
+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.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.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Crm 数据权限 Convert
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmPermissionConvert {
+
+    CrmPermissionConvert INSTANCE = Mappers.getMapper(CrmPermissionConvert.class);
+
+    CrmPermissionDO convert(CrmPermissionCreateReqBO createBO);
+
+    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, 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());
+                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;
+        });
+    }
+
+    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()));
+        });
+        return permissions;
+    }
+
+}
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/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/《芋道 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/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..435bf1995
--- /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,103 @@
+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.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;
+    /**
+     * 商机状态类型编号
+     *
+     *  关联 {@link CrmBusinessStatusTypeDO#getId()}
+     */
+    private Long statusTypeId;
+    /**
+     * 商机状态编号
+     *
+     * 关联 {@link CrmBusinessStatusDO#getId()}
+     */
+    private Long statusId;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime contactNextTime;
+    /**
+     * 客户编号
+     *
+     * TODO @ljileo:这个字段,后续要写下关联的实体哈
+     */
+    private Long customerId;
+    /**
+     * 预计成交日期
+     */
+    private LocalDateTime dealTime;
+    /**
+     * 商机金额
+     *
+     */
+    private Integer price;
+    /**
+     * 整单折扣
+     *
+     */
+    private Integer discountPercent;
+    /**
+     * 产品总金额
+     *
+     */
+    private Integer productPrice;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 1赢单2输单3无效
+     *
+     * TODO @lijie:搞个枚举;
+     */
+    private Integer endStatus;
+    /**
+     * 结束时的备注
+     */
+    private String endRemark;
+    /**
+     * 最后跟进时间
+     */
+    private LocalDateTime contactLastTime;
+    /**
+     * 跟进状态
+     *
+     * TODO @lijie:目前就是 Boolean;是否跟进
+     */
+    private Integer followUpStatus;
+
+}
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/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..3a1b66ad0
--- /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,46 @@
+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;
+    /**
+     * 状态类型编号
+     *
+     * // TODO @ljlleo:要写下关联字段噢
+     */
+    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..fbc2e6857
--- /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,42 @@
+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;
+    // TODO @ljlleo:List 存储哈
+    /**
+     * 使用的部门编号
+     */
+    private String deptIds;
+    /**
+     * 开启状态
+     */
+    // 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/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..592301b44
--- /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,80 @@
+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;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+// TODO 芋艿:字段的顺序,需要整理下;
+/**
+ * 线索 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;
+    /**
+     * 转化状态
+     */
+    private Boolean transformStatus;
+    /**
+     * 跟进状态
+     */
+    private Boolean followUpStatus;
+    /**
+     * 线索名称
+     */
+    private String name;
+    /**
+     * 客户 id
+     *
+     * 关联 {@link CrmCustomerDO#getId()}
+     */
+    private Long customerId;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime contactNextTime;
+    /**
+     * 电话
+     */
+    private String telephone;
+    /**
+     * 手机号
+     */
+    private String mobile;
+    /**
+     * 地址
+     */
+    private String address;
+    /**
+     * 最后跟进时间 TODO 添加跟进记录时更新该值
+     */
+    private LocalDateTime contactLastTime;
+    /**
+     * 备注
+     */
+    private String remark;
+
+    // TODO 芋艿:客户级别;
+    // TODO 芋艿:线索来源;
+    // TODO 芋艿:客户行业;
+
+}
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/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..f98da52bc
--- /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,103 @@
+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.LocalDateTime;
+
+/**
+ * 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 {
+
+    // TODO @zyna:这个字段的顺序,是不是整理下;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime nextTime;
+    /**
+     * 手机号
+     */
+    private String mobile;
+    /**
+     * 电话
+     */
+    private String telephone;
+    /**
+     * 电子邮箱
+     */
+    private String email;
+    /**
+     * 客户编号
+     */
+    private Long customerId;
+    /**
+     * 地址
+     */
+    private String address;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 最后跟进时间
+     */
+    private LocalDateTime lastTime;
+    // TODO @zyna:这个放在最前面吧
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+    // TODO @zyna:直接上级,最好写下它关联的字段,例如说这个,应该关联 ContactDO 的 id 字段
+    /**
+     * 直属上级
+     */
+    private Long parentId;
+    /**
+     * 姓名
+     */
+    private String name;
+    /**
+     * 职位
+     */
+    private String post;
+    /**
+     * QQ
+     */
+    private Long qq;
+    // TODO @zyna:wechat
+    /**
+     * 微信
+     */
+    private String webchat;
+    // TODO @zyna:关联的枚举
+    /**
+     * 性别
+     */
+    private Integer sex;
+    // TODO @zyna:这个字段改成 master 哈;
+    /**
+     * 是否关键决策人
+     */
+    private Boolean policyMakers;
+    // TODO @zyna:应该是 Long
+    /**
+     * 负责人用户编号
+     */
+    private String ownerUserId;
+
+}
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/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..f32786791
--- /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,92 @@
+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 String no;
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+    /**
+     * 结束时间
+     */
+    private LocalDateTime endTime;
+    /**
+     * 合同金额
+     */
+    private Integer price;
+    /**
+     * 整单折扣
+     */
+    private Integer discountPercent;
+    /**
+     * 产品总金额
+     */
+    private Integer productPrice;
+    /**
+     * 联系人编号
+     */
+    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/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/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..c5826b7c3
--- /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,123 @@
+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;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+// TODO 芋艿:调整下字段
+
+/**
+ * 客户 DO
+ *
+ * @author Wanwan
+ */
+@TableName(value = "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;
+    /**
+     * 跟进状态
+     */
+    private Boolean followUpStatus;
+    /**
+     * 锁定状态
+     */
+    private Boolean lockStatus;
+    /**
+     * 成交状态
+     */
+    private Boolean dealStatus;
+    /**
+     * 所属行业
+     *
+     * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_INDUSTRY}
+     */
+    private Integer industryId;
+    /**
+     * 客户等级
+     *
+     * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_LEVEL}
+     */
+    private Integer level;
+    /**
+     * 客户来源
+     *
+     * 对应字典 {@link DictTypeConstants#CRM_CUSTOMER_SOURCE}
+     */
+    private Integer source;
+    /**
+     * 手机
+     */
+    private String mobile;
+    /**
+     * 电话
+     */
+    private String telephone;
+    /**
+     * 网址
+     */
+    private String website;
+    /**
+     * QQ
+     */
+    private String qq;
+    /**
+     * wechat
+     */
+    private String wechat;
+    /**
+     * email
+     */
+    private String email;
+    /**
+     * 客户描述
+     */
+    private String description;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 负责人的用户编号
+     *
+     * 关联 AdminUserDO 的 id 字段
+     */
+    private Long ownerUserId;
+    /**
+     * 地区编号
+     */
+    private Integer areaId;
+    /**
+     * 详细地址
+     */
+    private String detailAddress;
+    /**
+     * 最后跟进时间
+     */
+    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/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..7e3b4cd15
--- /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,50 @@
+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/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..5be934b04
--- /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,60 @@
+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;
+    /**
+     * 规则类型
+     *
+     * TODO @wanwan:搞个枚举哈;
+     */
+    private Integer type;
+    /**
+     * 规则适用人群
+     */
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> userIds;
+    /**
+     * 规则适用部门
+     */
+    @TableField(typeHandler = LongListTypeHandler.class)
+    private List<Long> deptIds;
+    /**
+     * 数量上限
+     */
+    private Integer maxCount;
+    /**
+     * 成交客户是否占有拥有客户数
+     *
+     * 当且仅当 {@link #type} 为 1 时,进行使用
+     */
+    private Boolean dealCountEnabled;
+
+}
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..55d583597
--- /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,70 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.permission;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+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.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * Crm 数据权限 DO
+ *
+ * @author HUIHUI
+ */
+@TableName("crm_permission")
+@KeySequence("crm_permission_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmPermissionDO extends BaseDO {
+
+    // TODO puhui999:是不是公海的数据,就不插入了;这样方便获取公海数据鸭
+    // TODO @puhui999:每个数据那的负责人,我想了下,还是存储的;
+    /**
+     * 当数据变为公海数据时,也就是数据团队成员中没有负责人的时候,将原本的负责人 userId 设置为 POOL_USER_ID 方便查询公海数据。
+     * 也就是说每条数据到最后都有一个负责人,如果有人领取则 userId 为领取人
+     */
+    public static final Long POOL_USER_ID = 0L;
+
+    /**
+     * ID
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 数据类型
+     *
+     * 枚举 {@link CrmBizTypeEnum}
+     */
+    private Integer bizType;
+    /**
+     * 数据编号
+     *
+     * 关联 {@link CrmBizTypeEnum} 对应模块 DO 的 id 字段
+     */
+    private Long bizId;
+
+    /**
+     * 团队成员
+     *
+     * 关联 AdminUser 的 id 字段
+     *
+     * 如果为公海数据的话会干掉此数据的负责人后设置为 {@link #POOL_USER_ID},领取人则上位负责人
+     * 例:客户放入公海后会干掉团队成员中的负责人,而其他团队成员则不受影响
+     */
+    private Long userId;
+
+    /**
+     * 权限级别
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    private Integer level;
+
+}
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..c44ef7553
--- /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,66 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.product;
+
+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 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 对应的类}
+     * // TODO @zange:这个写个枚举类,然后 {@link关联下
+     */
+    private Integer status;
+    /**
+     * 产品分类 ID
+     * // TODO @zange:这个要写下关联 CategoryDO 的 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/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/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..8dcfaac36
--- /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 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 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
+     * // 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/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
new file mode 100644
index 000000000..a8c3e3fed
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java
@@ -0,0 +1,116 @@
+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;
+
+/**
+ * 回款管理 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 CrmReceivableDO extends BaseDO {
+
+    /**
+     * ID
+     */
+    @TableId
+    private Long id;
+    /**
+     * 回款编号
+     */
+    private String no;
+    // TODO @liuhongfeng:“对应实体”,参考别的模块,关联 {@link TableField.MetaInfo#getJdbcType()}
+    /**
+     * 回款计划
+     *
+     * TODO @liuhongfeng:这个字段什么时候更新,也可以写下
+     *
+     * 对应实体 {@link CrmReceivablePlanDO}
+     */
+    private Long planId;
+    /**
+     * 客户 ID
+     *
+     * 对应实体 {@link cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO}
+     */
+    private Long customerId;
+    /**
+     * 合同 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}
+     */
+    private Integer checkStatus;
+    /**
+     * 工作流编号
+     *
+     * TODO @liuhongfeng:这个字段,后续要写下关联的实体哈
+     */
+    private Long processInstanceId;
+    /**
+     * 回款日期
+     */
+    private LocalDateTime returnTime;
+    // TODO @liuhongfeng:少个枚举
+    /**
+     * 回款方式
+     */
+    private String returnType;
+    /**
+     * 回款金额
+     */
+    private Integer price;
+    // TODO @liuhongfeng:少关联实体;
+    /**
+     * 负责人
+     */
+    private Long ownerUserId;
+    // TODO @liuhongfeng:应该不需要 batchId 字段
+    /**
+     * 批次
+     */
+    private Long batchId;
+    /**
+     * 显示顺序
+     */
+    private Integer sort;
+    // TODO 芋艿:dataScope、dataScopeDeptIds 在想下;
+    /**
+     * 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)
+     */
+    private Integer dataScope;
+    /**
+     * 数据范围(指定部门数组)
+     */
+    private String dataScopeDeptIds;
+    /**
+     * 状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
+     *
+     */
+    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/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
new file mode 100644
index 000000000..52c0d2745
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivablePlanDO.java
@@ -0,0 +1,104 @@
+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;
+
+/**
+ * 回款计划 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 CrmReceivablePlanDO extends BaseDO {
+
+    /**
+     * ID
+     */
+    @TableId
+    private Long id;
+    /**
+     * 期数
+     */
+    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;
+    /**
+     * 计划回款日期
+     */
+    private LocalDateTime returnTime;
+    /**
+     * 提前几天提醒
+     */
+    private Integer remindDays;
+    /**
+     * 提醒日期
+     */
+    private LocalDateTime remindTime;
+    /**
+     * 客户 ID
+     *
+     * TODO @liuhongfeng:少关联实体;
+     */
+    private Long customerId;
+    /**
+     * 合同 ID
+     *
+     * TODO @liuhongfeng:少关联实体;
+     */
+    private Long contractId;
+    /**
+     * 负责人 ID
+     *
+     * TODO @liuhongfeng:少关联实体;
+     */
+    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/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/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..760a52593
--- /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,50 @@
+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.Collection;
+import java.util.List;
+
+/**
+ * 商机 Mapper
+ *
+ * @author ljlleo
+ */
+@Mapper
+public interface CrmBusinessMapper extends BaseMapperX<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));
+    }
+
+    // TODO @puhui999:selectList 噢;
+    default List<CrmBusinessDO> selectPage(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())
+                .betweenIfPresent(CrmBusinessDO::getCreateTime, reqVO.getCreateTime())
+                .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/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/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..258477e4d
--- /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,41 @@
+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()) TODO 报错,临时注释掉
+                .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/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..8f9fea6c3
--- /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,44 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.clue;
+
+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.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 java.util.List;
+
+/**
+ * 线索 Mapper
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
+
+    default PageResult<CrmClueDO> selectPage(CrmCluePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CrmClueDO>()
+                .likeIfPresent(CrmClueDO::getName, reqVO.getName())
+                .likeIfPresent(CrmClueDO::getTelephone, reqVO.getTelephone())
+                .likeIfPresent(CrmClueDO::getMobile, reqVO.getMobile())
+                .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())
+                .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/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/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..ef336c37e
--- /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,64 @@
+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>()
+                .betweenIfPresent(ContactDO::getNextTime, reqVO.getNextTime())
+                .eqIfPresent(ContactDO::getMobile, reqVO.getMobile())
+                .eqIfPresent(ContactDO::getTelephone, reqVO.getTelephone())
+                .eqIfPresent(ContactDO::getEmail, reqVO.getEmail())
+                .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>()
+                .betweenIfPresent(ContactDO::getNextTime, reqVO.getNextTime())
+                .eqIfPresent(ContactDO::getMobile, reqVO.getMobile())
+                .eqIfPresent(ContactDO::getTelephone, reqVO.getTelephone())
+                .eqIfPresent(ContactDO::getEmail, reqVO.getEmail())
+                .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/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/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/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/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..683df3581
--- /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,30 @@
+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.CrmCustomerPageReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+
+/**
+ * 客户 Mapper
+ *
+ * @author Wanwan
+ */
+@Mapper
+public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
+
+    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/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/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/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/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..ad54a7604
--- /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,50 @@
+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;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * crm 数据权限 mapper
+ *
+ * @author HUIHUI
+ */
+@Mapper
+public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
+
+    default CrmPermissionDO selectByBizTypeAndBizIdByUserId(Integer bizType, Long bizId, Long userId) {
+        return selectOne(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getBizType, bizType)
+                .eq(CrmPermissionDO::getBizId, bizId)
+                .eq(CrmPermissionDO::getUserId, userId));
+    }
+
+    default List<CrmPermissionDO> selectByBizTypeAndBizId(Integer bizType, Long bizId) {
+        return selectList(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getBizType, bizType)
+                .eq(CrmPermissionDO::getBizId, bizId));
+    }
+
+    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));
+    }
+
+    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/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/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/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/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/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/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/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..09526e070
--- /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,47 @@
+package cn.iocoder.yudao.module.crm.framework.core.annotations;
+
+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;
+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 类型
+     */
+    CrmBizTypeEnum bizType();
+
+    /**
+     * CRM 类型扩展,通过 Spring EL 表达式获取到 {@link #bizType()}
+     *
+     * 目的:用于 CrmPermissionController 团队权限校验
+     */
+    String bizTypeValue() default "";
+
+    /**
+     * 数据编号,通过 Spring EL 表达式获取
+     * TODO 数据权限完成后去除 default ""
+     */
+    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
new file mode 100644
index 000000000..d1d30dca4
--- /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,166 @@
+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.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;
+
+/**
+ * Crm 数据权限校验 AOP 切面
+ *
+ * @author HUIHUI
+ */
+@Component
+@Aspect
+@Slf4j
+public class CrmPermissionAspect {
+
+    @Resource
+    private LocalVariableTableParameterNameDiscoverer discoverer;
+    @Resource
+    private SpelExpressionParser parser;
+
+    @Resource
+    private CrmPermissionService crmPermissionService;
+
+    /**
+     * 获得用户编号
+     *
+     * @return 用户编号
+     */
+    private static Long getUserId() {
+        return WebFrameworkUtils.getLoginUserId();
+    }
+
+    @Before("@annotation(crmPermission)")
+    public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) throws NoSuchMethodException {
+        // TODO 芋艿:临时,方便大家调试
+        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;
+        }
+        // 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;
+            }
+            if (CrmPermissionLevelEnum.isRead(userPermission.getLevel())) { // 校验当前用户是否有读权限
+                return;
+            }
+            // 如果查询数据的话拥有写权限的也能查询
+            if (CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限
+                return;
+            }
+        }
+        // 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());
+    }
+
+
+    // TODO @puhui999:这块看看能不能用 SpringExpressionUtils 工具类;
+    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..efbf5d1d5
--- /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,21 @@
+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;
+
+// TODO @puhui999:SpringExpressionUtils
+/**
+ * 注册 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/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/enums/CrmBizTypeEnum.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
new file mode 100644
index 000000000..e4fc3dbbf
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/enums/CrmBizTypeEnum.java
@@ -0,0 +1,49 @@
+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;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * Crm 类型枚举
+ *
+ * @author HUIHUI
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmBizTypeEnum implements IntArrayValuable {
+
+    // TODO @puhui999:如果类似 CrmBizPermission 的 bizType 需要为空,可以设置它是数组,参考 Telephone 的 payload
+    CRM_PERMISSION(0, "团队"), // CrmPermissionController 中使用
+    CRM_LEADS(1, "线索"),
+    CRM_CUSTOMER(2, "客户"),
+    CRM_CONTACTS(3, "联系人"),
+    CRM_BUSINESS(5, "商机"),
+    CRM_CONTRACT(6, "合同");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmBizTypeEnum::getType).toArray();
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 名称
+     */
+    private final String name;
+
+    public static String getNameByType(Integer type) {
+        CrmBizTypeEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(CrmBizTypeEnum.values()),
+                item -> ObjUtil.equal(item.type, type));
+        return typeEnum == null ? null : typeEnum.getName();
+    }
+
+    @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..ff09f766f
--- /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,51 @@
+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/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/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..ec5dbe1d4
--- /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,82 @@
+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 javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 商机 Service 接口
+ *
+ * @author ljlleo
+ */
+public interface CrmBusinessService {
+
+    /**
+     * 创建商机
+     *
+     * @param createReqVO 创建信息
+     * @param userId      用户编号
+     * @return 编号
+     */
+    Long createBusiness(@Valid CrmBusinessCreateReqVO createReqVO, Long userId);
+
+    /**
+     * 更新商机
+     *
+     * @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 分页查询
+     * @param userId    用户编号
+     * @return 商机分页
+     */
+    PageResult<CrmBusinessDO> getBusinessPage(CrmBusinessPageReqVO pageReqVO, Long userId);
+
+    /**
+     * 获得商机列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 商机列表
+     */
+    List<CrmBusinessDO> getBusinessList(CrmBusinessExportReqVO exportReqVO);
+
+    /**
+     * 商机转移
+     *
+     * @param reqVO  请求
+     * @param 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
new file mode 100644
index 000000000..fc322fac5
--- /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,136 @@
+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.*;
+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;
+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 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;
+
+/**
+ * 商机 Service 实现类
+ *
+ * @author ljlleo
+ */
+@Service
+@Validated
+public class CrmBusinessServiceImpl implements CrmBusinessService {
+
+    @Resource
+    private CrmBusinessMapper businessMapper;
+
+    @Resource
+    private CrmPermissionService crmPermissionService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createBusiness(CrmBusinessCreateReqVO createReqVO, Long userId) {
+        // 插入
+        CrmBusinessDO business = CrmBusinessConvert.INSTANCE.convert(createReqVO);
+        businessMapper.insert(business);
+
+        // 创建数据权限
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType())
+                .setBizId(business.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+
+        // 返回
+        return business.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, bizId = "#updateReqVO.id",
+            level = CrmPermissionLevelEnum.WRITE)
+    public void updateBusiness(CrmBusinessUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateBusinessExists(updateReqVO.getId());
+        // 更新
+        CrmBusinessDO updateObj = CrmBusinessConvert.INSTANCE.convert(updateReqVO);
+        businessMapper.updateById(updateObj);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, level = CrmPermissionLevelEnum.WRITE)
+    public void deleteBusiness(Long id) {
+        // 校验存在
+        validateBusinessExists(id);
+        // 删除
+        businessMapper.deleteById(id);
+    }
+
+    private CrmBusinessDO validateBusinessExists(Long id) {
+        CrmBusinessDO crmBusiness = businessMapper.selectById(id);
+        if (crmBusiness == null) {
+            throw exception(BUSINESS_NOT_EXISTS);
+        }
+        return crmBusiness;
+    }
+
+    @Override
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, level = CrmPermissionLevelEnum.READ)
+    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, Long userId) {
+        // 1. 获取当前用户能看的分页数据
+        // TODO @puhui999:如果业务的数据量比较大,in 太多可能有性能问题噢;看看是不是搞成 join 连表了;可以微信讨论下;
+        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. 获取商机分页数据
+        return businessMapper.selectPage(pageReqVO, ids);
+    }
+
+    @Override
+    public List<CrmBusinessDO> getBusinessList(CrmBusinessExportReqVO exportReqVO) {
+        return businessMapper.selectPage(exportReqVO);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void transferBusiness(CrmBusinessTransferReqVO reqVO, Long userId) {
+        // 1 校验商机是否存在
+        validateBusinessExists(reqVO.getId());
+
+        // 2. 数据权限转移
+        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/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/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..44b3e9f1b
--- /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..f428c3836
--- /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,98 @@
+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) {
+        // TODO ljlleo:name 应该需要唯一哈;
+        // 插入
+        CrmBusinessStatusTypeDO businessStatusType = CrmBusinessStatusTypeConvert.INSTANCE.convert(createReqVO);
+        businessStatusTypeMapper.insert(businessStatusType);
+        // 返回
+        return businessStatusType.getId();
+    }
+
+    @Override
+    public void updateBusinessStatusType(CrmBusinessStatusTypeUpdateReqVO updateReqVO) {
+        // TODO ljlleo:name 应该需要唯一哈;
+        // 校验存在
+        validateBusinessStatusTypeExists(updateReqVO.getId());
+        // 更新
+        CrmBusinessStatusTypeDO updateObj = CrmBusinessStatusTypeConvert.INSTANCE.convert(updateReqVO);
+        businessStatusTypeMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteBusinessStatusType(Long id) {
+        // 校验存在
+        validateBusinessStatusTypeExists(id);
+        // TODO 艿艿:这里在看看,是不是要校验业务是否在使用;
+        // 删除
+        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/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..362637e30
--- /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,99 @@
+package cn.iocoder.yudao.module.crm.service.clue;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.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 cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+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;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
+
+/**
+ * 线索 Service 实现类
+ *
+ * @author Wanwan
+ */
+@Service
+@Validated
+public class CrmClueServiceImpl implements CrmClueService {
+
+    @Resource
+    private CrmClueMapper clueMapper;
+    @Resource
+    private CrmCustomerService customerService;
+
+    @Override
+    public Long createClue(CrmClueCreateReqVO createReqVO) {
+        // 校验客户是否存在
+        customerService.validateCustomer(createReqVO.getCustomerId());
+        // 插入
+        CrmClueDO clue = CrmClueConvert.INSTANCE.convert(createReqVO);
+        clueMapper.insert(clue);
+        // 返回
+        return clue.getId();
+    }
+
+    @Override
+    public void updateClue(CrmClueUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateClueExists(updateReqVO.getId());
+        // 校验客户是否存在
+        customerService.validateCustomer(updateReqVO.getCustomerId());
+
+        // 更新
+        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/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/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..c27c1541a
--- /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,81 @@
+package cn.iocoder.yudao.module.crm.service.contact;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.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 接口
+ *
+ * @author 芋道源码
+ */
+public interface ContactService {
+
+    /**
+     * 创建crm联系人
+     *
+     * @param createReqVO 创建信息
+     * @param userId 用户编号
+     * @return 编号
+     */
+    Long createContact(@Valid ContactCreateReqVO createReqVO, Long userId);
+
+    /**
+     * 更新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);
+
+    /**
+     * 获取所有联系人列表,只返回姓名和id
+     * @return 所有联系人列表
+     */
+    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
new file mode 100644
index 000000000..279bbc568
--- /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,114 @@
+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.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 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ContactServiceImpl implements ContactService {
+
+    @Resource
+    private ContactMapper contactMapper;
+
+    @Resource
+    private CrmPermissionService crmPermissionService;
+
+    @Override // TODO @zyna:新增和修改时,关联字段要校验,例如说 直属上级,是不是真的存在;
+    public Long createContact(ContactCreateReqVO createReqVO, Long userId) {
+        // 插入
+        ContactDO contact = ContactConvert.INSTANCE.convert(createReqVO);
+        contactMapper.insert(contact);
+
+        // 创建数据权限
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CONTACTS.getType())
+                .setBizId(contact.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+
+        // 返回
+        return contact.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, level = CrmPermissionLevelEnum.WRITE)
+    public void updateContact(ContactUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateContactExists(updateReqVO.getId());
+        // 更新
+        ContactDO updateObj = ContactConvert.INSTANCE.convert(updateReqVO);
+        contactMapper.updateById(updateObj);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, level = CrmPermissionLevelEnum.WRITE)
+    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
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACTS, level = CrmPermissionLevelEnum.READ)
+    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);
+    }
+
+    @Override
+    public List<ContactDO> allContactList() {
+        return contactMapper.selectList();
+    }
+
+}
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/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..201684bd8
--- /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,81 @@
+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.*;
+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 创建信息
+     * @param userId      用户编号
+     * @return 编号
+     */
+    Long createContract(@Valid ContractCreateReqVO createReqVO, Long userId);
+
+    /**
+     * 更新合同
+     *
+     * @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);
+
+    /**
+     * 合同转移
+     *
+     * @param reqVO  请求
+     * @param 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
new file mode 100644
index 000000000..393487d97
--- /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,121 @@
+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.*;
+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.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.CONTRACT_NOT_EXISTS;
+
+/**
+ * 合同 Service 实现类
+ *
+ * @author dhb52
+ */
+@Service
+@Validated
+public class ContractServiceImpl implements ContractService {
+
+    @Resource
+    private ContractMapper contractMapper;
+
+    @Resource
+    private CrmPermissionService crmPermissionService;
+
+    @Override
+    public Long createContract(ContractCreateReqVO createReqVO, Long userId) {
+        // 插入
+        ContractDO contract = ContractConvert.INSTANCE.convert(createReqVO);
+        contractMapper.insert(contract);
+
+        // 创建数据权限
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType())
+                .setBizId(contract.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+
+        // 返回
+        return contract.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, level = CrmPermissionLevelEnum.WRITE)
+    public void updateContract(ContractUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateContractExists(updateReqVO.getId());
+        // 更新
+        ContractDO updateObj = ContractConvert.INSTANCE.convert(updateReqVO);
+        contractMapper.updateById(updateObj);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, level = CrmPermissionLevelEnum.WRITE)
+    public void deleteContract(Long id) {
+        // 校验存在
+        validateContractExists(id);
+        // 删除
+        contractMapper.deleteById(id);
+    }
+
+    private ContractDO validateContractExists(Long id) {
+        ContractDO contract = contractMapper.selectById(id);
+        if (contract == null) {
+            throw exception(CONTRACT_NOT_EXISTS);
+        }
+        return contract;
+    }
+
+    @Override
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, level = CrmPermissionLevelEnum.READ)
+    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);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void transferContract(CrmContractTransferReqVO reqVO, Long userId) {
+        // 1 校验合同是否存在
+        validateContractExists(reqVO.getId());
+
+        // 2. 数据权限转移
+        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/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/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..30e3873ab
--- /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,29 @@
+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..1a08adbf5
--- /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,65 @@
+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;
+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;
+
+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 实现类
+ *
+ * @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) {
+        // TODO @wanwan:看下 @AssertTrue 的逻辑;
+        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)) {
+            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/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..6aa21477e
--- /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,106 @@
+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.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 客户 Service 接口
+ *
+ * @author Wanwan
+ */
+public interface CrmCustomerService {
+
+    /**
+     * 创建客户
+     *
+     * @param createReqVO 创建信息
+     * @param userId      用户编号
+     * @return 编号
+     */
+    Long createCustomer(@Valid CrmCustomerCreateReqVO createReqVO, Long userId);
+
+    /**
+     * 更新客户
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateCustomer(@Valid CrmCustomerUpdateReqVO updateReqVO);
+
+    /**
+     * 删除客户
+     *
+     * @param id 编号
+     */
+    void deleteCustomer(Long id);
+
+    /**
+     * 获得客户
+     *
+     * @param id 编号
+     * @return 客户
+     */
+    CrmCustomerDO getCustomer(Long id);
+
+    /**
+     * 获得客户分页
+     *
+     * @param pageReqVO 分页查询
+     * @param userId    用户编号
+     * @return 客户分页
+     */
+    PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId);
+
+    /**
+     * 获得客户列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 客户列表
+     */
+    List<CrmCustomerDO> getCustomerList(CrmCustomerExportReqVO exportReqVO);
+
+    /**
+     * 校验客户是否存在
+     *
+     * @param customerId 客户 id
+     * @return 客户
+     */
+    CrmCustomerDO validateCustomer(Long customerId);
+
+    /**
+     * 客户转移
+     *
+     * @param reqVO  请求
+     * @param userId 用户编号
+     */
+    void transferCustomer(CrmCustomerTransferReqVO reqVO, Long userId);
+
+    /**
+     * 锁定/解锁客户
+     *
+     * @param updateReqVO 更新信息
+     */
+    void lockCustomer(@Valid CrmCustomerUpdateReqVO updateReqVO);
+
+    // TODO @xiaqing:根据 controller 的建议,改下
+    /**
+     * 领取公海客户
+     *
+     * @param ids 要领取的客户 id
+     */
+    void receive(List<Long>ids);
+
+    // TODO @xiaqing:根据 controller 的建议,改下
+    /**
+     * 分配公海客户
+     *
+     * @param cIds 要分配的客户 id
+     * @param ownerId 分配的负责人id
+     * @author xiaqing
+     */
+    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
new file mode 100644
index 000000000..ebb499128
--- /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,216 @@
+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.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;
+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;
+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.*;
+
+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.*;
+
+/**
+ * 客户 Service 实现类
+ *
+ * @author Wanwan
+ */
+@Service
+@Validated
+public class CrmCustomerServiceImpl implements CrmCustomerService {
+
+    @Resource
+    private CrmCustomerMapper customerMapper;
+    @Resource
+    private CrmPermissionService crmPermissionService;
+
+    @Override
+    public Long createCustomer(CrmCustomerCreateReqVO createReqVO, Long userId) {
+        // 插入
+        CrmCustomerDO customer = CrmCustomerConvert.INSTANCE.convert(createReqVO);
+        customerMapper.insert(customer);
+
+        // 创建数据权限
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType())
+                .setBizId(customer.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+
+        // 返回
+        return customer.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
+    public void updateCustomer(CrmCustomerUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateCustomerExists(updateReqVO.getId());
+
+        // 更新
+        CrmCustomerDO updateObj = CrmCustomerConvert.INSTANCE.convert(updateReqVO);
+        customerMapper.updateById(updateObj);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
+    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
+    @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, 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 获取当前用户能看的分页数据
+        // TODO @puhui999:如果业务的数据量比较大,in 太多可能有性能问题噢;看看是不是搞成 join 连表了;可以微信讨论下;
+        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();
+        }
+
+        // 2. 获取客户分页数据
+        return customerMapper.selectPage(pageReqVO, ids);
+    }
+
+    @Override
+    public List<CrmCustomerDO> getCustomerList(CrmCustomerExportReqVO exportReqVO) {
+        //return customerMapper.selectList(exportReqVO);
+        return Collections.emptyList();
+    }
+
+    /**
+     * 校验客户是否存在
+     *
+     * @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;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void transferCustomer(CrmCustomerTransferReqVO reqVO, Long userId) {
+        // 1. 校验合同是否存在
+        validateCustomer(reqVO.getId());
+
+        // 2. 数据权限转移
+        crmPermissionService.transferPermission(
+                CrmCustomerConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_CUSTOMER.getType()));
+
+        // 3. TODO 记录转移日志
+    }
+
+    @Override
+    public void lockCustomer(CrmCustomerUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateCustomerExists(updateReqVO.getId());
+        // TODO @Joey:可以校验下,如果已经对应的锁定状态,报个业务异常;原因是:后续这个业务会记录操作日志,会记录多了;
+        // TODO @芋艿:业务完善,增加锁定上限;
+
+        // 更新
+        CrmCustomerDO updateObj = CrmCustomerConvert.INSTANCE.convert(updateReqVO);
+        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){
+        // 先一次性校验完成客户是否可用
+        // TODO @xiaqing:批量一次性加载客户列表,然后去逐个校验;
+        for (Long cId : cIds) {
+            //校验是否存在
+            validateCustomerExists(cId);
+            //todo 校验是否已有负责人
+            validCustomerOwnerExist(cId);
+            //todo 校验是否锁定
+            validCustomerIsLocked(cId);
+            //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);
+    }
+
+    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_ALREADY_DEAL);
+        }
+    }
+
+}
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/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..9f3f3087f
--- /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,93 @@
+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.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;
+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;
+
+/**
+ * 客户限制配置 Service 实现类
+ *
+ * @author Wanwan
+ */
+@Service
+@Validated
+public class CrmCustomerLimitConfigServiceImpl implements CrmCustomerLimitConfigService {
+
+    @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);
+        // 返回
+        return customerLimitConfig.getId();
+    }
+
+    @Override
+    public void updateCustomerLimitConfig(CrmCustomerLimitConfigUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateCustomerLimitConfigExists(updateReqVO.getId());
+        validateUserAndDept(updateReqVO.getUserIds(), updateReqVO.getDeptIds());
+        // 更新
+        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);
+        }
+    }
+
+    /**
+     * 校验入参的用户和部门
+     *
+     * @param userIds 用户 ids
+     * @param deptIds 部门 ids
+     */
+    private void validateUserAndDept(Collection<Long> userIds, Collection<Long> deptIds) {
+        deptApi.validateDeptList(deptIds);
+        adminUserApi.validateUserList(userIds);
+    }
+
+}
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..96ac1e145
--- /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,115 @@
+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 javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * crm 数据权限 Service 接口
+ *
+ * @author HUIHUI
+ */
+public interface CrmPermissionService {
+
+    /**
+     * 创建数据权限
+     *
+     * @param createBO 创建信息
+     * @return 编号
+     */
+    Long createPermission(@Valid CrmPermissionCreateReqBO createBO);
+
+    /**
+     * 更新数据权限
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updatePermission(CrmPermissionUpdateReqVO updateReqVO);
+
+    /**
+     * 删除数据权限
+     *
+     * @param ids 编号
+     */
+    void deletePermission(Collection<Long> ids);
+
+    /**
+     * 获取用户数据权限通过 数据类型 x 某个数据 x 用户编号
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @param userId  用户编号,AdminUser#id
+     * @return Crm 数据权限
+     */
+    CrmPermissionDO getPermissionByBizTypeAndBizIdAndUserId(Integer bizType, Long bizId, Long userId);
+
+    /**
+     * 获取用户数据权限通过 权限编号 x 用户编号
+     *
+     * @param id     权限编号
+     * @param userId 用户编号
+     * @return 数据权限
+     */
+    CrmPermissionDO getPermissionByIdAndUserId(Long id, Long userId);
+
+    /**
+     * 获取数据权限列表,通过 数据类型 x 某个数据
+     *
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @return Crm 数据权限列表
+     */
+    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);
+
+    /**
+     * 数据权限转移
+     *
+     * @param crmPermissionTransferReqBO 数据权限转移请求
+     */
+    void transferPermission(@Valid CrmPermissionTransferReqBO crmPermissionTransferReqBO);
+
+    /**
+     * 获取用户参与的模块数据列表
+     *
+     * @param bizType 模块类型
+     * @param userId  用户编号
+     * @return 模块数据列表
+     */
+    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
new file mode 100644
index 000000000..681c1654b
--- /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,172 @@
+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;
+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.system.api.user.AdminUserApi;
+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.Collections;
+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;
+
+// TODO @puhui999:尽量规避用“团队”这个词哈;这个只是我们给前端展示用的;
+/**
+ * CRM 数据权限 Service 接口实现类
+ *
+ * @author HUIHUI
+ */
+@Service
+@Validated
+public class CrmPermissionServiceImpl implements CrmPermissionService {
+
+    @Resource
+    private CrmPermissionMapper crmPermissionMapper;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @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();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePermission(CrmPermissionUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateCrmPermissionExists(updateReqVO.getIds());
+
+        List<CrmPermissionDO> updateDO = CrmPermissionConvert.INSTANCE.convertList(updateReqVO);
+        crmPermissionMapper.updateBatch(updateDO);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deletePermission(Collection<Long> ids) {
+        // 校验存在
+        validateCrmPermissionExists(ids);
+
+        // 删除
+        crmPermissionMapper.deleteBatchIds(ids);
+    }
+
+    @Override
+    public CrmPermissionDO getPermissionByBizTypeAndBizIdAndUserId(Integer bizType, Long bizId, Long userId) {
+        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);
+    }
+
+    @Override
+    public List<CrmPermissionDO> getPermissionByBizTypeAndBizIdsAndLevel(Integer bizType, Collection<Long> bizIds, Integer level) {
+        return crmPermissionMapper.selectListByBizTypeAndBizIdsAndLevel(bizType, bizIds, level);
+    }
+
+    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(),
+                transferReqBO.getBizId(), transferReqBO.getUserId());
+        String crmName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType());
+        // TODO 校验是否为超级管理员 || 1
+        if (oldPermission == null || !isOwner(oldPermission.getLevel())) {
+            throw exception(CRM_PERMISSION_DENIED, crmName);
+        }
+        // 1.1 校验转移对象是否已经是该负责人
+        if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) {
+            throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, crmName);
+        }
+        // 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()); // 获取所有团队成员
+        // 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())
+                    .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+        } else { // 存在则修改权限级别
+            crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId())
+                    .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+        }
+        // 2.2. 老负责人处理
+        if (transferReqBO.getOldOwnerPermissionLevel() != null) { // 加入团队
+            crmPermissionMapper.updateById(new CrmPermissionDO().setId(oldPermission.getId())
+                    .setLevel(transferReqBO.getOldOwnerPermissionLevel())); // 设置加入团队后的级别
+            return;
+        }
+        crmPermissionMapper.deleteById(oldPermission.getId());
+    }
+
+    @Override
+    public List<CrmPermissionDO> getPermissionListByBizTypeAndUserId(Integer bizType, Long userId) {
+        return crmPermissionMapper.selectListByBizTypeAndUserId(bizType, userId);
+    }
+
+    @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) { // 不存在则模块数据也不存在
+            throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, CrmBizTypeEnum.getNameByType(bizType));
+        }
+
+        crmPermissionMapper.updateById(new CrmPermissionDO().setId(permission.getId()).setUserId(userId));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    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));
+    }
+
+}
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..fce59cac1
--- /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,43 @@
+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;
+
+    /**
+     * 权限级别
+     */
+    @NotNull(message = "权限级别不能为空")
+    @InEnum(CrmPermissionLevelEnum.class)
+    private Integer level;
+
+}
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
new file mode 100644
index 000000000..3e3873b47
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/bo/CrmPermissionTransferReqBO.java
@@ -0,0 +1,49 @@
+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 CrmPermissionTransferReqBO {
+
+    /**
+     * 当前登录用户编号
+     */
+    @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;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 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
new file mode 100644
index 000000000..9cd198cdf
--- /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,36 @@
+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.CrmPermissionLevelEnum;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * crm 数据权限 Update Req BO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class CrmPermissionUpdateReqBO {
+
+    /**
+     * 数据权限编号
+     */
+    @NotNull(message = "数据权限编号不能为空")
+    private Long id;
+
+    /**
+     * 当前登录用户编号
+     */
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    /**
+     * 权限级别
+     */
+    @NotNull(message = "权限级别不能为空")
+    @InEnum(CrmPermissionLevelEnum.class)
+    private Integer level;
+
+}
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..e9a49d04f
--- /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,108 @@
+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());
+        // TODO @zange:需要校验 categoryId 是否存在;
+        // 插入
+        ProductDO product = ProductConvert.INSTANCE.convert(createReqVO);
+        productMapper.insert(product);
+        // 返回
+        return product.getId();
+    }
+
+    @Override
+    public void updateProduct(ProductUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateProductExists(updateReqVO.getId(), updateReqVO.getNo());
+        // TODO @zange:需要校验 categoryId 是否存在;
+        // 更新
+        ProductDO updateObj = ProductConvert.INSTANCE.convert(updateReqVO);
+        productMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteProduct(Long id) {
+        // 校验存在
+        validateProductExists(id, null);
+        // 删除
+        productMapper.deleteById(id);
+    }
+
+    // TODO @zange:validateProductExists 要不只校验是否存在;然后是否 no 重复,交给 validateProductNo,名字改成 validateProductNoDuplicate,和别的模块保持一致哈;
+    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/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/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..01a51d425
--- /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,76 @@
+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 org.springframework.validation.annotation.Validated;
+
+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.PRODUCT_CATEGORY_NOT_EXISTS;
+
+// TODO @zange:这个类所在的包,放到 product 下;
+/**
+ * 产品分类 Service 实现类
+ *
+ * @author ZanGe丶
+ */
+@Service
+@Validated
+public class ProductCategoryServiceImpl implements ProductCategoryService {
+
+    @Resource
+    private ProductCategoryMapper productCategoryMapper;
+
+    @Override
+    public Long createProductCategory(ProductCategoryCreateReqVO createReqVO) {
+        // TODO zange:参考 mall: ProductCategoryServiceImpl 补充下必要的参数校验;
+        // 插入
+        ProductCategoryDO productCategory = ProductCategoryConvert.INSTANCE.convert(createReqVO);
+        productCategoryMapper.insert(productCategory);
+        // 返回
+        return productCategory.getId();
+    }
+
+    @Override
+    public void updateProductCategory(ProductCategoryUpdateReqVO updateReqVO) {
+        // TODO zange:参考 mall: ProductCategoryServiceImpl 补充下必要的参数校验;
+        // 校验存在
+        validateProductCategoryExists(updateReqVO.getId());
+        // 更新
+        ProductCategoryDO updateObj = ProductCategoryConvert.INSTANCE.convert(updateReqVO);
+        productCategoryMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteProductCategory(Long id) {
+        // TODO zange:参考 mall: ProductCategoryServiceImpl 补充下必要的参数校验;
+        // 校验存在
+        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/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.java
new file mode 100644
index 000000000..b8c472fa3
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanService.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.CrmReceivablePlanDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 回款计划 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface CrmReceivablePlanService {
+
+    /**
+     * 创建回款计划
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createReceivablePlan(@Valid CrmReceivablePlanCreateReqVO createReqVO);
+
+    /**
+     * 更新回款计划
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateReceivablePlan(@Valid CrmReceivablePlanUpdateReqVO updateReqVO);
+
+    /**
+     * 删除回款计划
+     *
+     * @param id 编号
+     */
+    void deleteReceivablePlan(Long id);
+
+    /**
+     * 获得回款计划
+     *
+     * @param id 编号
+     * @return 回款计划
+     */
+    CrmReceivablePlanDO getReceivablePlan(Long id);
+
+    /**
+     * 获得回款计划列表
+     *
+     * @param ids 编号
+     * @return 回款计划列表
+     */
+    List<CrmReceivablePlanDO> getReceivablePlanList(Collection<Long> ids);
+
+    /**
+     * 获得回款计划分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 回款计划分页
+     */
+    PageResult<CrmReceivablePlanDO> getReceivablePlanPage(CrmReceivablePlanPageReqVO pageReqVO);
+
+    /**
+     * 获得回款计划列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 回款计划列表
+     */
+    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/CrmReceivablePlanServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
new file mode 100644
index 000000000..30523a095
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
@@ -0,0 +1,130 @@
+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.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.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;
+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.*;
+
+// TODO @liuhongfeng:参考 CrmReceivableServiceImpl 写的 todo 哈;
+/**
+ * 回款计划 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
+
+    @Resource
+    private CrmReceivablePlanMapper crmReceivablePlanMapper;
+    @Resource
+    private ContractService contractService;
+    @Resource
+    private CrmCustomerService crmCustomerService;
+
+    @Override
+    public Long createReceivablePlan(CrmReceivablePlanCreateReqVO createReqVO) {
+        // 插入
+        CrmReceivablePlanDO receivablePlan = CrmReceivablePlanConvert.INSTANCE.convert(createReqVO);
+        if (ObjectUtil.isNull(receivablePlan.getStatus())){
+            receivablePlan.setStatus(CommonStatusEnum.ENABLE.getStatus());
+        }
+        if (ObjectUtil.isNull(receivablePlan.getCheckStatus())){
+            receivablePlan.setCheckStatus(AuditStatusEnum.AUDIT_NEW.getValue());
+        }
+
+        checkReceivablePlan(receivablePlan);
+
+        crmReceivablePlanMapper.insert(receivablePlan);
+        // 返回
+        return receivablePlan.getId();
+    }
+
+    private void checkReceivablePlan(CrmReceivablePlanDO 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(CrmReceivablePlanUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateReceivablePlanExists(updateReqVO.getId());
+
+        // 更新
+        CrmReceivablePlanDO updateObj = CrmReceivablePlanConvert.INSTANCE.convert(updateReqVO);
+        crmReceivablePlanMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteReceivablePlan(Long id) {
+        // 校验存在
+        validateReceivablePlanExists(id);
+        // 删除
+        crmReceivablePlanMapper.deleteById(id);
+    }
+
+    private void validateReceivablePlanExists(Long id) {
+        if (crmReceivablePlanMapper.selectById(id) == null) {
+            throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmReceivablePlanDO getReceivablePlan(Long id) {
+        return crmReceivablePlanMapper.selectById(id);
+    }
+
+    @Override
+    public List<CrmReceivablePlanDO> getReceivablePlanList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return crmReceivablePlanMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CrmReceivablePlanDO> getReceivablePlanPage(CrmReceivablePlanPageReqVO pageReqVO) {
+        return crmReceivablePlanMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    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/CrmReceivableService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
new file mode 100644
index 000000000..8875faaa9
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.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.CrmReceivableDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 回款管理 Service 接口
+ *
+ * @author 赤焰
+ */
+public interface CrmReceivableService {
+
+    /**
+     * 创建回款管理
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createReceivable(@Valid CrmReceivableCreateReqVO createReqVO);
+
+    /**
+     * 更新回款管理
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateReceivable(@Valid CrmReceivableUpdateReqVO updateReqVO);
+
+    /**
+     * 删除回款管理
+     *
+     * @param id 编号
+     */
+    void deleteReceivable(Long id);
+
+    /**
+     * 获得回款管理
+     *
+     * @param id 编号
+     * @return 回款管理
+     */
+    CrmReceivableDO getReceivable(Long id);
+
+    /**
+     * 获得回款管理列表
+     *
+     * @param ids 编号
+     * @return 回款管理列表
+     */
+    List<CrmReceivableDO> getReceivableList(Collection<Long> ids);
+
+    /**
+     * 获得回款管理分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 回款管理分页
+     */
+    PageResult<CrmReceivableDO> getReceivablePage(CrmReceivablePageReqVO pageReqVO);
+
+    /**
+     * 获得回款管理列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 回款管理列表
+     */
+    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/CrmReceivableServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
new file mode 100644
index 000000000..08d97d8fe
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -0,0 +1,142 @@
+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.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.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;
+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.*;
+
+/**
+ * 回款管理 Service 实现类
+ *
+ * @author 赤焰
+ */
+@Service
+@Validated
+public class CrmReceivableServiceImpl implements CrmReceivableService {
+
+    // TODO @liuhongfeng:crm 前缀,变量可以不带哈;
+    @Resource
+    private CrmReceivableMapper crmReceivableMapper;
+    @Resource
+    private ContractService contractService;
+    @Resource
+    private CrmCustomerService crmCustomerService;
+    @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());
+        }
+        if (ObjectUtil.isNull(receivable.getCheckStatus())){
+            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);
+        }
+
+        ContractDO contract = contractService.getContract(receivable.getContractId());
+        if(ObjectUtil.isNull(contract)){
+            throw exception(CONTRACT_NOT_EXISTS);
+        }
+
+        CrmCustomerDO customer = crmCustomerService.getCustomer(receivable.getCustomerId());
+        if(ObjectUtil.isNull(customer)){
+            throw exception(CUSTOMER_NOT_EXISTS);
+        }
+
+        CrmReceivablePlanDO receivablePlan = crmReceivablePlanService.getReceivablePlan(receivable.getPlanId());
+        if(ObjectUtil.isNull(receivablePlan)){
+            throw exception(RECEIVABLE_PLAN_NOT_EXISTS);
+        }
+
+    }
+
+    @Override
+    public void updateReceivable(CrmReceivableUpdateReqVO updateReqVO) {
+        // 校验存在
+        validateReceivableExists(updateReqVO.getId());
+
+        // 更新
+        CrmReceivableDO updateObj = CrmReceivableConvert.INSTANCE.convert(updateReqVO);
+        crmReceivableMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteReceivable(Long id) {
+        // 校验存在
+        validateReceivableExists(id);
+        // 删除
+        crmReceivableMapper.deleteById(id);
+    }
+
+    private void validateReceivableExists(Long id) {
+        if (crmReceivableMapper.selectById(id) == null) {
+            throw exception(RECEIVABLE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CrmReceivableDO getReceivable(Long id) {
+        return crmReceivableMapper.selectById(id);
+    }
+
+    @Override
+    public List<CrmReceivableDO> getReceivableList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return ListUtil.empty();
+        }
+        return crmReceivableMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CrmReceivableDO> getReceivablePage(CrmReceivablePageReqVO pageReqVO) {
+        return crmReceivableMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CrmReceivableDO> getReceivableList(CrmReceivableExportReqVO exportReqVO) {
+        return crmReceivableMapper.selectList(exportReqVO);
+    }
+
+}
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-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..9f199954e
--- /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,264 @@
+package cn.iocoder.yudao.module.crm.service.business;
+
+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.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.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;
+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, getLoginUserId());
+        // 断言
+        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.setCreateTime(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)));
+       // 测试 createTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setCreateTime(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.setCreateTime(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)));
+       // 测试 createTime 不匹配
+       businessMapper.insert(cloneIgnoreId(dbBusiness, o -> o.setCreateTime(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));
+    }
+
+}
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..9a59f3bd2
--- /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,168 @@
+package cn.iocoder.yudao.module.crm.service.businessstatus;
+
+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.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..bf778b989
--- /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-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..4757e921d
--- /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,214 @@
+package cn.iocoder.yudao.module.crm.service.clue;
+
+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.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 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.CLUE_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+// TODO 芋艿:单测后续补;
+/**
+ * {@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.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)));
+       // 测试 contactLastTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setContactLastTime(null)));
+       // 测试 createTime 不匹配
+       clueMapper.insert(cloneIgnoreId(dbClue, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmCluePageReqVO reqVO = new CrmCluePageReqVO();
+       reqVO.setName(null);
+       reqVO.setTelephone(null);
+       reqVO.setMobile(null);
+
+       // 调用
+       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.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)));
+       // 测试 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/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..1406b5916
--- /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,196 @@
+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.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;
+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, getLoginUserId());
+        // 断言
+        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));
+    }
+
+}
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..4908f79e2
--- /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,173 @@
+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.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;
+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} 的单元测试类
+ *
+ * @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, getLoginUserId());
+        // 断言
+        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, 1L);
+        // 断言
+        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/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..827fd0e02
--- /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,119 @@
+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.*;
+
+// TODO 芋艿:单测后面搞
+/**
+ * {@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/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivablePlanServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivablePlanServiceImplTest.java
new file mode 100644
index 000000000..09993f899
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivablePlanServiceImplTest.java
@@ -0,0 +1,225 @@
+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.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;
+
+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.RECEIVABLE_PLAN_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+// TODO 芋艿:后续,需要补充测试用例
+/**
+ * {@link CrmReceivablePlanServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(CrmReceivablePlanServiceImpl.class)
+public class CrmCrmReceivablePlanServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmReceivablePlanServiceImpl receivablePlanService;
+
+    @Resource
+    private CrmReceivablePlanMapper crmReceivablePlanMapper;
+
+    @Test
+    public void testCreateReceivablePlan_success() {
+        // 准备参数
+        CrmReceivablePlanCreateReqVO reqVO = randomPojo(CrmReceivablePlanCreateReqVO.class);
+
+        // 调用
+        Long receivablePlanId = receivablePlanService.createReceivablePlan(reqVO);
+        // 断言
+        assertNotNull(receivablePlanId);
+        // 校验记录的属性是否正确
+        CrmReceivablePlanDO receivablePlan = crmReceivablePlanMapper.selectById(receivablePlanId);
+        assertPojoEquals(reqVO, receivablePlan);
+    }
+
+    @Test
+    public void testUpdateReceivablePlan_success() {
+        // mock 数据
+        CrmReceivablePlanDO dbReceivablePlan = randomPojo(CrmReceivablePlanDO.class);
+        crmReceivablePlanMapper.insert(dbReceivablePlan);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmReceivablePlanUpdateReqVO reqVO = randomPojo(CrmReceivablePlanUpdateReqVO.class, o -> {
+            o.setId(dbReceivablePlan.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        receivablePlanService.updateReceivablePlan(reqVO);
+        // 校验是否更新正确
+        CrmReceivablePlanDO receivablePlan = crmReceivablePlanMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, receivablePlan);
+    }
+
+    @Test
+    public void testUpdateReceivablePlan_notExists() {
+        // 准备参数
+        CrmReceivablePlanUpdateReqVO reqVO = randomPojo(CrmReceivablePlanUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> receivablePlanService.updateReceivablePlan(reqVO), RECEIVABLE_PLAN_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteReceivablePlan_success() {
+        // mock 数据
+        CrmReceivablePlanDO dbReceivablePlan = randomPojo(CrmReceivablePlanDO.class);
+        crmReceivablePlanMapper.insert(dbReceivablePlan);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbReceivablePlan.getId();
+
+        // 调用
+        receivablePlanService.deleteReceivablePlan(id);
+       // 校验数据不存在了
+       assertNull(crmReceivablePlanMapper.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 数据
+       CrmReceivablePlanDO dbReceivablePlan = randomPojo(CrmReceivablePlanDO.class, o -> { // 等会查询到
+           o.setPeriod(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);
+       });
+       crmReceivablePlanMapper.insert(dbReceivablePlan);
+       // 测试 Period 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setPeriod(null)));
+       // 测试 status 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
+       // 测试 checkStatus 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCheckStatus(null)));
+       // 测试 returnTime 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setReturnTime(null)));
+       // 测试 remindDays 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindDays(null)));
+       // 测试 remindTime 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindTime(null)));
+       // 测试 customerId 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCustomerId(null)));
+       // 测试 contractId 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setContractId(null)));
+       // 测试 ownerUserId 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setOwnerUserId(null)));
+       // 测试 remark 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemark(null)));
+       // 测试 createTime 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmReceivablePlanPageReqVO reqVO = new CrmReceivablePlanPageReqVO();
+       reqVO.setStatus(null);
+       reqVO.setCheckStatus(null);
+       reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setRemindTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCustomerId(null);
+       reqVO.setContractId(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<CrmReceivablePlanDO> 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 数据
+       CrmReceivablePlanDO dbReceivablePlan = randomPojo(CrmReceivablePlanDO.class, o -> { // 等会查询到
+           o.setPeriod(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);
+       });
+       crmReceivablePlanMapper.insert(dbReceivablePlan);
+       // 测试 Period 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setPeriod(null)));
+       // 测试 status 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setStatus(null)));
+       // 测试 checkStatus 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCheckStatus(null)));
+       // 测试 returnTime 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setReturnTime(null)));
+       // 测试 remindDays 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindDays(null)));
+       // 测试 remindTime 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemindTime(null)));
+       // 测试 customerId 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCustomerId(null)));
+       // 测试 contractId 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setContractId(null)));
+       // 测试 ownerUserId 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setOwnerUserId(null)));
+       // 测试 remark 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setRemark(null)));
+       // 测试 createTime 不匹配
+       crmReceivablePlanMapper.insert(cloneIgnoreId(dbReceivablePlan, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmReceivablePlanExportReqVO reqVO = new CrmReceivablePlanExportReqVO();
+       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));
+
+       // 调用
+       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/CrmCrmReceivableServiceImplTest.java b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivableServiceImplTest.java
new file mode 100644
index 000000000..6d65231b1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/receivable/CrmCrmReceivableServiceImplTest.java
@@ -0,0 +1,266 @@
+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.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;
+
+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.RECEIVABLE_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+// TODO 芋艿:等实现完,在校验下;
+/**
+ * {@link CrmReceivableServiceImpl} 的单元测试类
+ *
+ * @author 赤焰
+ */
+@Import(CrmReceivableServiceImpl.class)
+public class CrmCrmReceivableServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CrmReceivableServiceImpl receivableService;
+
+    @Resource
+    private CrmReceivableMapper crmReceivableMapper;
+
+    @Test
+    public void testCreateReceivable_success() {
+        // 准备参数
+        CrmReceivableCreateReqVO reqVO = randomPojo(CrmReceivableCreateReqVO.class);
+
+        // 调用
+        Long receivableId = receivableService.createReceivable(reqVO);
+        // 断言
+        assertNotNull(receivableId);
+        // 校验记录的属性是否正确
+        CrmReceivableDO receivable = crmReceivableMapper.selectById(receivableId);
+        assertPojoEquals(reqVO, receivable);
+    }
+
+    @Test
+    public void testUpdateReceivable_success() {
+        // mock 数据
+        CrmReceivableDO dbReceivable = randomPojo(CrmReceivableDO.class);
+        crmReceivableMapper.insert(dbReceivable);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CrmReceivableUpdateReqVO reqVO = randomPojo(CrmReceivableUpdateReqVO.class, o -> {
+            o.setId(dbReceivable.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        receivableService.updateReceivable(reqVO);
+        // 校验是否更新正确
+        CrmReceivableDO receivable = crmReceivableMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, receivable);
+    }
+
+    @Test
+    public void testUpdateReceivable_notExists() {
+        // 准备参数
+        CrmReceivableUpdateReqVO reqVO = randomPojo(CrmReceivableUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> receivableService.updateReceivable(reqVO), RECEIVABLE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteReceivable_success() {
+        // mock 数据
+        CrmReceivableDO dbReceivable = randomPojo(CrmReceivableDO.class);
+        crmReceivableMapper.insert(dbReceivable);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbReceivable.getId();
+
+        // 调用
+        receivableService.deleteReceivable(id);
+       // 校验数据不存在了
+       assertNull(crmReceivableMapper.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 数据
+       CrmReceivableDO dbReceivable = randomPojo(CrmReceivableDO.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);
+       });
+       crmReceivableMapper.insert(dbReceivable);
+       // 测试 no 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setNo(null)));
+       // 测试 planId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPlanId(null)));
+       // 测试 customerId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCustomerId(null)));
+       // 测试 contractId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setContractId(null)));
+       // 测试 checkStatus 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCheckStatus(null)));
+       // 测试 processInstanceId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setProcessInstanceId(null)));
+       // 测试 returnTime 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnTime(null)));
+       // 测试 returnType 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnType(null)));
+       // 测试 price 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPrice(null)));
+       // 测试 ownerUserId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setOwnerUserId(null)));
+       // 测试 batchId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setBatchId(null)));
+       // 测试 sort 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setSort(null)));
+       // 测试 dataScope 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScope(null)));
+       // 测试 dataScopeDeptIds 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScopeDeptIds(null)));
+       // 测试 status 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setStatus(null)));
+       // 测试 remark 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setRemark(null)));
+       // 测试 createTime 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmReceivablePageReqVO reqVO = new CrmReceivablePageReqVO();
+       reqVO.setNo(null);
+       reqVO.setPlanId(null);
+       reqVO.setCustomerId(null);
+       reqVO.setContractId(null);
+       reqVO.setCheckStatus(null);
+       reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setReturnType(null);
+       reqVO.setPrice(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setStatus(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<CrmReceivableDO> 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 数据
+       CrmReceivableDO dbReceivable = randomPojo(CrmReceivableDO.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);
+       });
+       crmReceivableMapper.insert(dbReceivable);
+       // 测试 no 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setNo(null)));
+       // 测试 planId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPlanId(null)));
+       // 测试 customerId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCustomerId(null)));
+       // 测试 contractId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setContractId(null)));
+       // 测试 checkStatus 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCheckStatus(null)));
+       // 测试 processInstanceId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setProcessInstanceId(null)));
+       // 测试 returnTime 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnTime(null)));
+       // 测试 returnType 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setReturnType(null)));
+       // 测试 price 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setPrice(null)));
+       // 测试 ownerUserId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setOwnerUserId(null)));
+       // 测试 batchId 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setBatchId(null)));
+       // 测试 sort 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setSort(null)));
+       // 测试 dataScope 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScope(null)));
+       // 测试 dataScopeDeptIds 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setDataScopeDeptIds(null)));
+       // 测试 status 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setStatus(null)));
+       // 测试 remark 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setRemark(null)));
+       // 测试 createTime 不匹配
+       crmReceivableMapper.insert(cloneIgnoreId(dbReceivable, o -> o.setCreateTime(null)));
+       // 准备参数
+       CrmReceivableExportReqVO reqVO = new CrmReceivableExportReqVO();
+       reqVO.setNo(null);
+       reqVO.setPlanId(null);
+       reqVO.setCustomerId(null);
+       reqVO.setContractId(null);
+       reqVO.setCheckStatus(null);
+       reqVO.setReturnTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setReturnType(null);
+       reqVO.setPrice(null);
+       reqVO.setOwnerUserId(null);
+       reqVO.setBatchId(null);
+       reqVO.setStatus(null);
+       reqVO.setRemark(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       List<CrmReceivableDO> 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/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>
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..138780eed
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/clean.sql
@@ -0,0 +1,11 @@
+DELETE FROM "crm_contract";
+
+DELETE FROM "crm_clue";
+
+DELETE FROM "crm_receivable";
+
+DELETE FROM "crm_receivable_plan";
+
+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
new file mode 100644
index 000000000..f94600db0
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/test/resources/sql/create_tables.sql
@@ -0,0 +1,141 @@
+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 '合同表';
+
+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 '线索表';
+
+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 '回款管理';
+
+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 '回款计划';
+
+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,
+  "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 '客户表';
+
+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-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..721cf19c0
--- /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,53 @@
+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.module.product.controller.admin.favorite.vo.ProductFavoritePageReqVO;
+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.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.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 javax.annotation.Resource;
+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
+@RequestMapping("/product/favorite")
+@Validated
+public class ProductFavoriteController {
+
+    @Resource
+    private ProductFavoriteService productFavoriteService;
+
+    @Resource
+    private ProductSpuService productSpuService;
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商品收藏分页")
+    @PreAuthorize("@ss.hasPermission('product:favorite:query')")
+    public CommonResult<PageResult<ProductFavoriteRespVO>> getFavoritePage(@Valid ProductFavoritePageReqVO pageVO) {
+        PageResult<ProductFavoriteDO> pageResult = productFavoriteService.getFavoritePage(pageVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty());
+        }
+        // 拼接数据
+        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/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..68b0a0a16
--- /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,19 @@
+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/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..3d78883ec
--- /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,18 @@
+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;
+
+@Schema(description = "管理后台 - 商品收藏分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+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/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..3c09aa8fc
--- /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,19 @@
+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;
+
+}
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;
 
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..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,5 +1,8 @@
 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.spu.ProductSpuDO;
@@ -34,4 +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 convert02(ProductSpuDO spu, ProductFavoriteDO favorite);
+
 }
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..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,6 +2,8 @@ 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;
@@ -21,6 +23,12 @@ public interface ProductFavoriteMapper extends BaseMapperX<ProductFavoriteDO> {
                 .orderByDesc(ProductFavoriteDO::getId));
     }
 
+    default PageResult<ProductFavoriteDO> selectPageByUserId(ProductFavoritePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ProductFavoriteDO>()
+                .eqIfPresent(ProductFavoriteDO::getUserId, reqVO.getUserId())
+                .orderByDesc(ProductFavoriteDO::getId));
+    }
+
     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..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
@@ -1,6 +1,7 @@
 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;
 
@@ -37,6 +38,13 @@ public interface ProductFavoriteService {
      */
     PageResult<ProductFavoriteDO> getFavoritePage(Long userId, @Valid AppFavoritePageReqVO reqVO);
 
+    /**
+     * 分页查询用户收藏列表
+     *
+     * @param reqVO 请求 vo
+     */
+    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 983cbf83c..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
@@ -1,6 +1,7 @@
 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;
@@ -54,6 +55,11 @@ public class ProductFavoriteServiceImpl implements ProductFavoriteService {
         return productFavoriteMapper.selectPageByUserAndType(userId, reqVO);
     }
 
+    @Override
+    public PageResult<ProductFavoriteDO> getFavoritePage(@Valid ProductFavoritePageReqVO reqVO) {
+        return productFavoriteMapper.selectPageByUserId(reqVO);
+    }
+
     @Override
     public ProductFavoriteDO getFavorite(Long userId, Long spuId) {
         return productFavoriteMapper.selectByUserIdAndSpuId(userId, spuId);
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-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..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
@@ -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_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_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
+    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, "转账单不处于待转账或转账中");
+
     // ========== 示例订单 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-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/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/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/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/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 212551f4d..c7be35f1a 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/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/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 014a7aae7..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,30 +1,36 @@
 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;
+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.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;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 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;
@@ -51,6 +57,8 @@ public class PayTransferServiceImpl implements PayTransferService {
     @Resource
     private PayChannelService channelService;
     @Resource
+    private PayNotifyService notifyService;
+    @Resource
     private PayNoRedisDAO noRedisDAO;
     @Resource
     private Validator validator;
@@ -70,56 +78,60 @@ public class PayTransferServiceImpl implements PayTransferService {
 
     @Override
     public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
-        // 1.1 校验转账单是否可以提交
-        validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId());
-        // 1.2 校验 App
-        appService.validPayApp(reqDTO.getAppId());
-        // 1.3 校验支付渠道是否有效
+        // 1.1 校验 App
+        PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
+        // 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("http://127.0.0.1:48080/admin-api/pay/todo"); // TODO 需要加个transfer Notify url
-        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)
@@ -133,34 +145,48 @@ public class PayTransferServiceImpl implements PayTransferService {
         if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) {
             notifyTransferClosed(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);
+        // 转账处理中的回调
+        if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) {
+            notifyTransferInProgress(channel, notify);
         }
+        // WAITING 状态无需处理
     }
 
-    private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
-        // 1. 更新 PayTransferDO 转账成功
-        Boolean transferred = updateTransferSuccess(channel, notify);
-        if (transferred) {
+    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;
         }
-        // 2. TODO 插入转账通知记录
+        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 Boolean updateTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
+
+    private void notifyTransferSuccess(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 +202,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 +221,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 +233,61 @@ public class PayTransferServiceImpl implements PayTransferService {
             throw exception(PAY_TRANSFER_STATUS_IS_NOT_PENDING);
         }
         log.info("[updateTransferClosed][transfer({}) 更新为关闭状态]", transfer.getId());
+
+        // 3. 插入转账通知记录
+        notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
+                transfer.getId());
+
     }
 
-    private void notifyTransferClosed(PayChannelDO channel, PayTransferRespDTO notify) {
-        //  更新 PayTransferDO 转账关闭
-        updateTransferClosed(channel, notify);
+    @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));
     }
 
     /**
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-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..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,10 +1,14 @@
 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.dal.dataobject.dept.PostDO;
 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 +26,10 @@ public class PostApiImpl implements PostApi {
         postService.validatePostList(ids);
     }
 
+    @Override
+    public List<PostRespDTO> getPostList(Collection<Long> ids) {
+        List<PostDO> list = postService.getPostList(ids);
+        return BeanUtils.toBean(list, PostRespDTO.class);
+    }
+
 }
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..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,29 @@ public class UserController {
         return success(new PageResult<>(userList, pageResult.getTotal()));
     }
 
+    // TODO @芋艿:看看这里怎么统一调整下;客户的选择组件;
+    @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();
+    }
+
     /**
      * 对密码进行加密
      *
diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index 87a961778..20b67725b 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -69,7 +69,7 @@
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
 
-<!--         商城相关模块。默认注释,保证编译速度-->
+        <!-- 商城相关模块。默认注释,保证编译速度-->
 <!--        <dependency>-->
 <!--            <groupId>cn.iocoder.boot</groupId>-->
 <!--            <artifactId>yudao-module-promotion-biz</artifactId>-->
@@ -89,6 +89,13 @@
 <!--            <groupId>cn.iocoder.boot</groupId>-->
 <!--            <artifactId>yudao-module-statistics-biz</artifactId>-->
 <!--            <version>${revision}</version>-->
+<!--        </dependency>-->
+
+        <!-- CRM 相关模块。默认注释,保证编译速度 -->
+<!--        <dependency>-->
+<!--            <groupId>cn.iocoder.boot</groupId>-->
+<!--            <artifactId>yudao-module-crm-biz</artifactId>-->
+<!--            <version>${revision}</version>-->
 <!--        </dependency>-->
 
         <!-- spring boot 配置所需依赖 -->
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/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/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>
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>