Преглед на файлове

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

YunaiV преди 1 година
родител
ревизия
9cba258d8f
променени са 100 файла, в които са добавени 3146 реда и са изтрити 390 реда
  1. 2 2
      pom.xml
  2. 46 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java
  3. 6 2
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java
  4. 5 5
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  5. 3 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java
  6. 0 41
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java
  7. 142 4
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java
  8. 21 0
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java
  9. 27 0
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/ExcelColumnSelect.java
  10. 0 27
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/enums/ExcelColumn.java
  11. 28 0
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/function/ExcelColumnSelectFunction.java
  12. 69 19
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java
  13. 1 19
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
  14. 6 3
      yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java
  15. 1 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogConfiguration.java
  16. 1 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  17. 5 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
  18. 3 3
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java
  19. 7 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionLevelEnum.java
  20. 2 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessSaveReqVO.java
  21. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessTransferReqVO.java
  22. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
  23. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractTransferReqVO.java
  24. 2 26
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
  25. 6 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java
  26. 8 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerTransferReqVO.java
  27. 13 9
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
  28. 0 14
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionCreateReqVO.java
  29. 23 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java
  30. 11 7
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionSaveReqVO.java
  31. 18 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java
  32. 19 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java
  33. 55 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http
  34. 101 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.java
  35. 52 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsPerformanceController.java
  36. 61 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsPortraitController.java
  37. 20 21
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsRankController.java
  38. 20 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerByUserBaseRespVO.java
  39. 48 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerContractSummaryRespVO.java
  40. 16 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByDateRespVO.java
  41. 16 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByUserRespVO.java
  42. 46 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerReqVO.java
  43. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerSummaryByDateRespVO.java
  44. 24 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerSummaryByUserRespVO.java
  45. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsFollowUpSummaryByDateRespVO.java
  46. 17 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsFollowUpSummaryByTypeRespVO.java
  47. 16 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsFollowUpSummaryByUserRespVO.java
  48. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsPoolSummaryByDateRespVO.java
  49. 16 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsPoolSummaryByUserRespVO.java
  50. 42 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/performance/CrmStatisticsPerformanceReqVO.java
  51. 24 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/performance/CrmStatisticsPerformanceRespVO.java
  52. 21 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticCustomerAreaRespVO.java
  53. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticCustomerIndustryRespVO.java
  54. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticCustomerLevelRespVO.java
  55. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticCustomerSourceRespVO.java
  56. 42 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticsPortraitReqVO.java
  57. 2 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/rank/CrmStatisticsRankReqVO.java
  58. 8 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/rank/CrmStatisticsRankRespVO.java
  59. 8 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
  60. 5 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/CrmContactMapper.java
  61. 6 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractMapper.java
  62. 5 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/permission/CrmPermissionMapper.java
  63. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
  64. 194 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java
  65. 41 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsPerformanceMapper.java
  66. 24 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsPortraitMapper.java
  67. 11 11
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsRankMapper.java
  68. 33 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/excel/core/AreaExcelColumnSelectFunction.java
  69. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/excel/package-info.java
  70. 3 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/package-info.java
  71. 3 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/package-info.java
  72. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/web/package-info.java
  73. 12 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
  74. 11 6
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
  75. 11 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactService.java
  76. 5 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java
  77. 12 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java
  78. 19 6
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
  79. 41 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
  80. 13 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
  81. 122 10
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
  82. 1 5
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java
  83. 8 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
  84. 31 14
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
  85. 96 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerService.java
  86. 323 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java
  87. 42 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceService.java
  88. 102 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
  89. 46 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitService.java
  90. 128 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitServiceImpl.java
  91. 11 11
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsRankService.java
  92. 19 19
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsRankServiceImpl.java
  93. 224 0
      yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
  94. 152 0
      yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
  95. 61 0
      yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPortraitMapper.xml
  96. 51 59
      yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsRankMapper.xml
  97. 7 7
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
  98. 7 4
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm
  99. 1 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
  100. 0 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStorePageReqVO.java

+ 2 - 2
pom.xml

@@ -16,12 +16,12 @@
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
 <!--        <module>yudao-module-member</module>-->
-<!--        <module>yudao-module-bpm</module>-->
+        <module>yudao-module-bpm</module>
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-mp</module>-->
 <!--        <module>yudao-module-pay</module>-->
 <!--        <module>yudao-module-mall</module>-->
-<!--        <module>yudao-module-crm</module>-->
+        <module>yudao-module-crm</module>
 <!--        <module>yudao-module-erp</module>-->
         <!-- 示例项目 -->
 <!--        <module>yudao-example</module>-->

+ 46 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.framework.common.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 时间间隔的枚举
+ *
+ * @author dhb52
+ */
+@Getter
+@AllArgsConstructor
+public enum DateIntervalEnum implements IntArrayValuable {
+
+    DAY(1, "天"),
+    WEEK(2, "周"),
+    MONTH(3, "月"),
+    QUARTER(4, "季度"),
+    YEAR(5, "年")
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DateIntervalEnum::getInterval).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer interval;
+    /**
+     * 名称
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static DateIntervalEnum valueOf(Integer interval) {
+        return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values());
+    }
+
+}

+ 6 - 2
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java

@@ -7,6 +7,7 @@ import com.google.common.cache.LoadingCache;
 
 import java.time.Duration;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
 /**
@@ -17,8 +18,11 @@ import java.util.concurrent.Executors;
 public class CacheUtils {
 
     public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
-        Executor executor = Executors.newCachedThreadPool(  // TODO 芋艿:可能要思考下,未来要不要做成可配置
-                TtlExecutors.getDefaultDisableInheritableThreadFactory()); // TTL 保证 ThreadLocal 可以透传
+        // 1. 使用 TTL 包装 ExecutorService,实现 ThreadLocal 的透传
+        // https://github.com/YunaiV/ruoyi-vue-pro/issues/432
+        ExecutorService executorService = Executors.newCachedThreadPool(); // TODO 芋艿:可能要思考下,未来要不要做成可配置
+        Executor executor = TtlExecutors.getTtlExecutorService(executorService);
+        // 2. 创建 Guava LoadingCache
         return CacheBuilder.newBuilder()
                 // 只阻塞当前数据加载线程,其他线程返回旧值
                 .refreshAfterWrite(duration)

+ 5 - 5
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java

@@ -78,7 +78,7 @@ public class CollectionUtils {
         if (CollUtil.isEmpty(from)) {
             return new ArrayList<>();
         }
-        return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
     }
 
     public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from,
@@ -87,7 +87,7 @@ public class CollectionUtils {
         if (CollUtil.isEmpty(from)) {
             return new ArrayList<>();
         }
-        return from.stream().map(mapper).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
     }
 
     public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) {
@@ -123,7 +123,7 @@ public class CollectionUtils {
         if (CollUtil.isEmpty(from)) {
             return new HashSet<>();
         }
-        return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+        return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
     }
 
     public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from,
@@ -132,7 +132,7 @@ public class CollectionUtils {
         if (CollUtil.isEmpty(from)) {
             return new HashSet<>();
         }
-        return from.stream().map(mapper).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+        return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
     }
 
     public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
@@ -315,4 +315,4 @@ public class CollectionUtils {
         return list.stream().flatMap(Collection::stream).collect(Collectors.toList());
     }
 
-}
+}

+ 3 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.util.collection;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
@@ -40,6 +41,7 @@ public class MapUtils {
 
     /**
      * 从哈希表查找到 key 对应的 value,然后进一步处理
+     * key 为 null 时, 不处理
      * 注意,如果查找到的 value 为 null 时,不进行处理
      *
      * @param map 哈希表
@@ -47,7 +49,7 @@ public class MapUtils {
      * @param consumer 进一步处理的逻辑
      */
     public static <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) {
-        if (CollUtil.isEmpty(map)) {
+        if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) {
             return;
         }
         V value = map.get(key);

+ 0 - 41
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java

@@ -27,8 +27,6 @@ public class DateUtils {
 
     public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
 
-    public static final String FORMAT_HOUR_MINUTE_SECOND = "HH:mm:ss";
-
     /**
      * 将 LocalDateTime 转换成 Date
      *
@@ -67,19 +65,11 @@ public class DateUtils {
         return new Date(System.currentTimeMillis() + duration.toMillis());
     }
 
-    public static boolean isExpired(Date time) {
-        return System.currentTimeMillis() > time.getTime();
-    }
-
     public static boolean isExpired(LocalDateTime time) {
         LocalDateTime now = LocalDateTime.now();
         return now.isAfter(time);
     }
 
-    public static long diff(Date endTime, Date startTime) {
-        return endTime.getTime() - startTime.getTime();
-    }
-
     /**
      * 创建指定时间
      *
@@ -136,37 +126,6 @@ public class DateUtils {
         return a.isAfter(b) ? a : b;
     }
 
-    /**
-     * 计算当期时间相差的日期
-     *
-     * @param field  日历字段.<br/>eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,<br/>Calendar.HOUR_OF_DAY等.
-     * @param amount 相差的数值
-     * @return 计算后的日志
-     */
-    public static Date addDate(int field, int amount) {
-        return addDate(null, field, amount);
-    }
-
-    /**
-     * 计算当期时间相差的日期
-     *
-     * @param date   设置时间
-     * @param field  日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等
-     * @param amount 相差的数值
-     * @return 计算后的日志
-     */
-    public static Date addDate(Date date, int field, int amount) {
-        if (amount == 0) {
-            return date;
-        }
-        Calendar c = Calendar.getInstance();
-        if (date != null) {
-            c.setTime(date);
-        }
-        c.add(field, amount);
-        return c.getTime();
-    }
-
     /**
      * 是否今天
      *

+ 142 - 4
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java

@@ -1,13 +1,18 @@
 package cn.iocoder.yudao.framework.common.util.date;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DatePattern;
 import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
 
-import java.time.Duration;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
+import java.time.*;
+import java.time.format.DateTimeParseException;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * 时间工具类,用于 {@link java.time.LocalDateTime}
@@ -21,6 +26,22 @@ public class LocalDateTimeUtils {
      */
     public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
 
+    /**
+     * 解析时间
+     *
+     * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功
+     *
+     * @param time 时间
+     * @return 时间字符串
+     */
+    public static LocalDateTime parse(String time) {
+        try {
+            return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN);
+        } catch (DateTimeParseException e) {
+            return LocalDateTimeUtil.parse(time);
+        }
+    }
+
     public static LocalDateTime addTime(Duration duration) {
         return LocalDateTime.now().plus(duration);
     }
@@ -54,6 +75,21 @@ public class LocalDateTimeUtils {
         return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)};
     }
 
+    /**
+     * 判指定断时间,是否在该时间范围内
+     *
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @param time 指定时间
+     * @return 是否
+     */
+    public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) {
+        if (startTime == null || endTime == null || time == null) {
+            return false;
+        }
+        return LocalDateTimeUtil.isIn(parse(time), startTime, endTime);
+    }
+
     /**
      * 判断当前时间是否在该时间范围内
      *
@@ -122,6 +158,16 @@ public class LocalDateTimeUtils {
         return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
     }
 
+    /**
+     * 获得指定日期所在季度
+     *
+     * @param date 日期
+     * @return 所在季度
+     */
+    public static int getQuarterOfYear(LocalDateTime date) {
+        return (date.getMonthValue() - 1) / 3 + 1;
+    }
+
     /**
      * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负
      *
@@ -168,4 +214,96 @@ public class LocalDateTimeUtils {
         return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
     }
 
+    public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
+                                                         LocalDateTime endTime,
+                                                         Integer interval) {
+        // 1.1 找到枚举
+        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
+        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
+        // 1.2 将时间对齐
+        startTime = LocalDateTimeUtil.beginOfDay(startTime);
+        endTime = LocalDateTimeUtil.endOfDay(endTime);
+
+        // 2. 循环,生成时间范围
+        List<LocalDateTime[]> timeRanges = new ArrayList<>();
+        switch (intervalEnum) {
+            case DateIntervalEnum.DAY:
+                while (startTime.isBefore(endTime)) {
+                    timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)});
+                    startTime = startTime.plusDays(1);
+                }
+                break;
+            case DateIntervalEnum.WEEK:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfWeek});
+                    startTime = endOfWeek.plusNanos(1);
+                }
+                break;
+            case DateIntervalEnum.MONTH:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfMonth});
+                    startTime = endOfMonth.plusNanos(1);
+                }
+                break;
+            case DateIntervalEnum.QUARTER:
+                while (startTime.isBefore(endTime)) {
+                    int quarterOfYear = getQuarterOfYear(startTime);
+                    LocalDateTime quarterEnd = quarterOfYear == 4
+                            ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1)
+                            : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, quarterEnd});
+                    startTime = quarterEnd.plusNanos(1);
+                }
+                break;
+            case DateIntervalEnum.YEAR:
+                while (startTime.isBefore(endTime)) {
+                    LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1);
+                    timeRanges.add(new LocalDateTime[]{startTime, endOfYear});
+                    startTime = endOfYear.plusNanos(1);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid interval: " + interval);
+        }
+        // 3. 兜底,最后一个时间,需要保持在 endTime 之前
+        LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges);
+        if (lastTimeRange != null) {
+            lastTimeRange[1] = endTime;
+        }
+        return timeRanges;
+    }
+
+    /**
+     * 格式化时间范围
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @param interval  时间间隔
+     * @return 时间范围
+     */
+    public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) {
+        // 1. 找到枚举
+        DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval);
+        Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval);
+
+        // 2. 循环,生成时间范围
+        switch (intervalEnum) {
+            case DateIntervalEnum.DAY:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN);
+            case DateIntervalEnum.WEEK:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN)
+                        + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime));
+            case DateIntervalEnum.MONTH:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN);
+            case DateIntervalEnum.QUARTER:
+                return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime));
+            case DateIntervalEnum.YEAR:
+                return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN);
+            default:
+                throw new IllegalArgumentException("Invalid interval: " + interval);
+        }
+    }
+
 }

+ 21 - 0
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java

@@ -11,6 +11,7 @@ import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 
 import java.time.Duration;
+import java.util.List;
 
 /**
  * 字典工具类
@@ -24,6 +25,7 @@ public class DictFrameworkUtils {
 
     private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();
 
+    // TODO @puhui999:GET_DICT_DATA_CACHE、GET_DICT_DATA_LIST_CACHE、PARSE_DICT_DATA_CACHE 这 3 个缓存是有点重叠,可以思考下,有没可能减少 1 个。微信讨论好私聊,再具体改哈
     /**
      * 针对 {@link #getDictDataLabel(String, String)} 的缓存
      */
@@ -38,6 +40,20 @@ public class DictFrameworkUtils {
 
             });
 
+    /**
+     * 针对 {@link #getDictDataLabelList(String)} 的缓存
+     */
+    private static final LoadingCache<String, List<String>> GET_DICT_DATA_LIST_CACHE = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<String, List<String>>() {
+
+                @Override
+                public List<String> load(String dictType) {
+                    return dictDataApi.getDictDataLabelList(dictType);
+                }
+
+            });
+
     /**
      * 针对 {@link #parseDictDataValue(String, String)} 的缓存
      */
@@ -67,6 +83,11 @@ public class DictFrameworkUtils {
         return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
     }
 
+    @SneakyThrows
+    public static List<String> getDictDataLabelList(String dictType) {
+        return GET_DICT_DATA_LIST_CACHE.get(dictType);
+    }
+
     @SneakyThrows
     public static String parseDictDataValue(String dictType, String label) {
         return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();

+ 27 - 0
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/ExcelColumnSelect.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.framework.excel.core.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * 给 Excel 列添加下拉选择数据
+ *
+ * 其中 {@link #dictType()} 和 {@link #functionName()} 二选一
+ *
+ * @author HUIHUI
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelColumnSelect {
+
+    /**
+     * @return 字典类型
+     */
+    String dictType() default "";
+
+    /**
+     * @return 获取下拉数据源的方法名称
+     */
+    String functionName() default "";
+
+}

+ 0 - 27
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/enums/ExcelColumn.java

@@ -1,27 +0,0 @@
-package cn.iocoder.yudao.framework.excel.core.enums;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-// TODO @puhui999:列表有办法通过 field name 么?主要考虑一个点,可能导入模版的顺序可能会变
-/**
- * Excel 列名枚举
- * 默认枚举 26 列列名如果有需求更多的列名请自行补充
- *
- * @author HUIHUI
- */
-@Getter
-@AllArgsConstructor
-public enum ExcelColumn {
-
-    A(0), B(1), C(2), D(3), E(4), F(5), G(6), H(7), I(8),
-    J(9), K(10), L(11), M(12), N(13), O(14), P(15), Q(16),
-    R(17), S(18), T(19), U(20), V(21), W(22), X(23), Y(24),
-    Z(25);
-
-    /**
-     * 列索引
-     */
-    private final int colNum;
-
-}

+ 28 - 0
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/function/ExcelColumnSelectFunction.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.excel.core.function;
+
+import java.util.List;
+
+/**
+ * Excel 列下拉数据源获取接口
+ *
+ * 为什么不直接解析字典还搞个接口?考虑到有的下拉数据不是从字典中获取的所有需要做一个兼容
+
+ * @author HUIHUI
+ */
+public interface ExcelColumnSelectFunction {
+
+    /**
+     * 获得方法名称
+     *
+     * @return 方法名称
+     */
+    String getName();
+
+    /**
+     * 获得列下拉数据源
+     *
+     * @return 下拉数据源
+     */
+    List<String> getOptions();
+
+}

+ 69 - 19
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java

@@ -2,25 +2,38 @@ package cn.iocoder.yudao.framework.excel.core.handler;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.hutool.poi.excel.ExcelUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.excel.core.enums.ExcelColumn;
+import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
+import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
+import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction;
+import com.alibaba.excel.annotation.ExcelProperty;
 import com.alibaba.excel.write.handler.SheetWriteHandler;
 import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
 import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.poi.hssf.usermodel.HSSFDataValidation;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.ss.util.CellRangeAddressList;
 
+import java.lang.reflect.Field;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 
 /**
  * 基于固定 sheet 实现下拉框
  *
  * @author HUIHUI
  */
+@Slf4j
 public class SelectSheetWriteHandler implements SheetWriteHandler {
 
     /**
@@ -36,21 +49,56 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
 
     private static final String DICT_SHEET_NAME = "字典sheet";
 
-    // TODO @puhui999:Map<ExcelColumn, List<String>> 可以么?之前用 keyvalue 的原因,返回给前端,无法用 linkedhashmap,默认 key 会乱序
-    private final List<KeyValue<ExcelColumn, List<String>>> selectMap;
+    /**
+     * key: 列 value: 下拉数据源
+     */
+    private final Map<Integer, List<String>> selectMap = new HashMap<>();
 
-    public SelectSheetWriteHandler(List<KeyValue<ExcelColumn, List<String>>> selectMap) {
-        if (CollUtil.isEmpty(selectMap)) {
-            this.selectMap = null;
+    public SelectSheetWriteHandler(Class<?> head) {
+        // 加载下拉数据获取接口
+        Map<String, ExcelColumnSelectFunction> beansMap = SpringUtil.getBeanFactory().getBeansOfType(ExcelColumnSelectFunction.class);
+        if (MapUtil.isEmpty(beansMap)) {
+            return;
+        }
+
+        // 解析下拉数据
+        int colIndex = 0;
+        for (Field field : head.getDeclaredFields()) {
+            if (field.isAnnotationPresent(ExcelColumnSelect.class)) {
+                ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
+                if (excelProperty != null && excelProperty.index() != -1) {
+                    colIndex = excelProperty.index();
+                }
+                getSelectDataList(colIndex, field);
+            }
+            colIndex++;
+        }
+    }
+
+    /**
+     * 获得下拉数据,并添加到 {@link #selectMap} 中
+     *
+     * @param colIndex 列索引
+     * @param field    字段
+     */
+    private void getSelectDataList(int colIndex, Field field) {
+        ExcelColumnSelect columnSelect = field.getAnnotation(ExcelColumnSelect.class);
+        String dictType = columnSelect.dictType();
+        String functionName = columnSelect.functionName();
+        Assert.isTrue(ObjectUtil.isNotEmpty(dictType) || ObjectUtil.isNotEmpty(functionName),
+                "Field({}) 的 @ExcelColumnSelect 注解,dictType 和 functionName 不能同时为空", field.getName());
+
+        // 情况一:使用 dictType 获得下拉数据
+        if (StrUtil.isNotEmpty(dictType)) { // 情况一: 字典数据 (默认)
+            selectMap.put(colIndex, DictFrameworkUtils.getDictDataLabelList(dictType));
             return;
         }
-        // 校验一下 key 是否唯一
-        Map<String, Long> nameCounts = selectMap.stream()
-                .collect(Collectors.groupingBy(item -> item.getKey().name(), Collectors.counting()));
-        Assert.isFalse(nameCounts.entrySet().stream().allMatch(entry -> entry.getValue() > 1), "下拉数据 key 重复请排查!!!");
 
-        selectMap.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错
-        this.selectMap = selectMap;
+        // 情况二:使用 functionName 获得下拉数据
+        Map<String, ExcelColumnSelectFunction> functionMap = SpringUtil.getApplicationContext().getBeansOfType(ExcelColumnSelectFunction.class);
+        ExcelColumnSelectFunction function = CollUtil.findOne(functionMap.values(), item -> item.getName().equals(functionName));
+        Assert.notNull(function, "未找到对应的 function({})", functionName);
+        selectMap.put(colIndex, function.getOptions());
     }
 
     @Override
@@ -62,18 +110,20 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
         // 1. 获取相应操作对象
         DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手
         Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿
+        List<KeyValue<Integer, List<String>>> keyValues = convertList(selectMap.entrySet(), entry -> new KeyValue<>(entry.getKey(), entry.getValue()));
+        keyValues.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错
 
         // 2. 创建数据字典的 sheet 页
         Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME);
-        for (KeyValue<ExcelColumn, List<String>> keyValue : selectMap) {
+        for (KeyValue<Integer, List<String>> keyValue : keyValues) {
             int rowLength = keyValue.getValue().size();
-            // 2.1 设置字典 sheet 页的值 每一列一个字典项
+            // 2.1 设置字典 sheet 页的值每一列一个字典项
             for (int i = 0; i < rowLength; i++) {
                 Row row = dictSheet.getRow(i);
                 if (row == null) {
                     row = dictSheet.createRow(i);
                 }
-                row.createCell(keyValue.getKey().getColNum()).setCellValue(keyValue.getValue().get(i));
+                row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i));
             }
             // 2.2 设置单元格下拉选择
             setColumnSelect(writeSheetHolder, workbook, helper, keyValue);
@@ -84,10 +134,10 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
      * 设置单元格下拉选择
      */
     private static void setColumnSelect(WriteSheetHolder writeSheetHolder, Workbook workbook, DataValidationHelper helper,
-                                        KeyValue<ExcelColumn, List<String>> keyValue) {
+                                        KeyValue<Integer, List<String>> keyValue) {
         // 1.1 创建可被其他单元格引用的名称
         Name name = workbook.createName();
-        String excelColumn = keyValue.getKey().name();
+        String excelColumn = ExcelUtil.indexToColName(keyValue.getKey());
         // 1.2 下拉框数据来源 eg:字典sheet!$B1:$B2
         String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + keyValue.getValue().size();
         name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字
@@ -97,7 +147,7 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
         DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束
         // 设置下拉单元格的首行、末行、首列、末列
         CellRangeAddressList rangeAddressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW,
-                keyValue.getKey().getColNum(), keyValue.getKey().getColNum());
+                keyValue.getKey(), keyValue.getKey());
         DataValidation validation = helper.createValidation(constraint, rangeAddressList);
         if (validation instanceof HSSFDataValidation) {
             validation.setSuppressDropDownArrow(false);

+ 1 - 19
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java

@@ -1,7 +1,5 @@
 package cn.iocoder.yudao.framework.excel.core.util;
 
-import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.excel.core.enums.ExcelColumn;
 import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
 import com.alibaba.excel.EasyExcel;
 import com.alibaba.excel.converters.longconverter.LongStringConverter;
@@ -34,27 +32,11 @@ public class ExcelUtils {
      */
     public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                  Class<T> head, List<T> data) throws IOException {
-        write(response, filename, sheetName, head, data, null);
-    }
-
-    /**
-     * 将列表以 Excel 响应给前端
-     *
-     * @param response  响应
-     * @param filename  文件名
-     * @param sheetName Excel sheet 名
-     * @param head      Excel head 头
-     * @param data      数据列表哦
-     * @param selectMap 下拉选择数据 Map<下拉所对应的列表名,下拉数据>
-     * @throws IOException 写入失败的情况
-     */
-    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
-                                 Class<T> head, List<T> data, List<KeyValue<ExcelColumn, List<String>>> selectMap) throws IOException {
         // 输出 Excel
         EasyExcel.write(response.getOutputStream(), head)
                 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
                 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
-                .registerWriteHandler(new SelectSheetWriteHandler(selectMap)) // 基于固定 sheet 实现下拉框
+                .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框
                 .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
                 .sheet(sheetName).doWrite(data);
         // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了

+ 6 - 3
yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java

@@ -40,12 +40,15 @@ public class TimeoutRedisCacheManager extends RedisCacheManager {
         // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间
         if (cacheConfig != null) {
             // 移除 # 后面的 : 以及后面的内容,避免影响解析
-            names[1] = StrUtil.subBefore(names[1], StrUtil.COLON, false);
+            String ttlStr = StrUtil.subBefore(names[1], StrUtil.COLON, false); // 获得 ttlStr 时间部分
+            names[1] = StrUtil.subAfter(names[1], ttlStr, false); // 移除掉 ttlStr 时间部分
             // 解析时间
-            Duration duration = parseDuration(names[1]);
+            Duration duration = parseDuration(ttlStr);
             cacheConfig = cacheConfig.entryTtl(duration);
         }
-        return super.createRedisCache(name, cacheConfig);
+
+        // 创建 RedisCache 对象,需要忽略掉 ttlStr
+        return super.createRedisCache(names[0] + names[1], cacheConfig);
     }
 
     /**

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogV2Configuration.java → yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogConfiguration.java

@@ -16,7 +16,7 @@ import org.springframework.context.annotation.Primary;
 @EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦
 @AutoConfiguration
 @Slf4j
-public class YudaoOperateLogV2Configuration {
+public class YudaoOperateLogConfiguration {
 
     @Bean
     @Primary

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1,3 +1,3 @@
 cn.iocoder.yudao.framework.security.config.YudaoSecurityAutoConfiguration
 cn.iocoder.yudao.framework.security.config.YudaoWebSecurityConfigurerAdapter
-cn.iocoder.yudao.framework.operatelog.config.YudaoOperateLogV2Configuration
+cn.iocoder.yudao.framework.operatelog.config.YudaoOperateLogConfiguration

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

@@ -15,6 +15,7 @@ public interface ErrorCodeConstants {
     ErrorCode CONTRACT_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_020_000_002, "合同提交审核失败,原因:合同没处在未提交状态");
     ErrorCode CONTRACT_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_000_003, "更新合同审核状态失败,原因:合同不是审核中状态");
     ErrorCode CONTRACT_NO_EXISTS = new ErrorCode(1_020_000_004, "生成合同序列号重复,请重试");
+    ErrorCode CONTRACT_DELETE_FAIL = new ErrorCode(1_020_000_005, "删除合同失败,原因:有被回款所使用");
 
     // ========== 线索管理 1-020-001-000 ==========
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
@@ -40,6 +41,7 @@ public interface ErrorCodeConstants {
     ErrorCode RECEIVABLE_NO_EXISTS = new ErrorCode(1_020_004_005, "生成回款序列号重复,请重试");
     ErrorCode RECEIVABLE_CREATE_FAIL_CONTRACT_NOT_APPROVE = new ErrorCode(1_020_004_006, "创建回款失败,原因:合同不是审核通过状态");
     ErrorCode RECEIVABLE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT = new ErrorCode(1_020_004_007, "创建回款失败,原因:回款金额超出合同金额,目前剩余可退:{} 元");
+    ErrorCode RECEIVABLE_DELETE_FAIL_IS_APPROVE = new ErrorCode(1_020_004_008, "删除回款失败,原因:回款审批已通过");
 
     // ========== 回款计划 1-020-005-000 ==========
     ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_020_005_000, "回款计划不存在");
@@ -72,6 +74,7 @@ public interface ErrorCodeConstants {
     ErrorCode CRM_PERMISSION_DELETE_DENIED = new ErrorCode(1_020_007_006, "删除数据权限失败,原因:没有权限");
     ErrorCode CRM_PERMISSION_DELETE_SELF_PERMISSION_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_007, "删除数据权限失败,原因:不能删除负责人");
     ErrorCode CRM_PERMISSION_CREATE_FAIL = new ErrorCode(1_020_007_008, "创建数据权限失败,原因:所加用户已有权限");
+    ErrorCode CRM_PERMISSION_CREATE_FAIL_EXISTS = new ErrorCode(1_020_007_009, "同时添加数据权限失败,原因:用户【{}】已有模块【{}】数据【{}】的【{}】权限");
 
     // ========== 产品 1_020_008_000 ==========
     ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_020_008_000, "产品不存在");
@@ -100,4 +103,6 @@ public interface ErrorCodeConstants {
     ErrorCode FOLLOW_UP_RECORD_NOT_EXISTS = new ErrorCode(1_020_013_000, "跟进记录不存在");
     ErrorCode FOLLOW_UP_RECORD_DELETE_DENIED = new ErrorCode(1_020_013_001, "删除跟进记录失败,原因:没有权限");
 
+    // ========== 数据统计 1_020_014_000 ==========
+
 }

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

@@ -142,11 +142,11 @@ public interface LogRecordConstants {
 
     String CRM_RECEIVABLE_TYPE = "CRM 回款";
     String CRM_RECEIVABLE_CREATE_SUB_TYPE = "创建回款";
-    String CRM_RECEIVABLE_CREATE_SUCCESS = "创建了合同【{getContractById{#receivable.contractId}}】的第【{{#receivable.period}}】期回款";
+    String CRM_RECEIVABLE_CREATE_SUCCESS = "创建了合同【{getContractById{#receivable.contractId}}】的{{#period != null ? '【第'+ #period +'期】' : '编号为【'+ #receivable.no +'】的'}}回款";
     String CRM_RECEIVABLE_UPDATE_SUB_TYPE = "更新回款";
-    String CRM_RECEIVABLE_UPDATE_SUCCESS = "更新了合同【{getContractById{#receivable.contractId}}】的第【{{#receivable.period}}】期回款: {_DIFF{#updateReqVO}}";
+    String CRM_RECEIVABLE_UPDATE_SUCCESS = "更新了合同【{getContractById{#receivable.contractId}}】的{{#period != null ? '【第'+ #period +'期】' : '编号为【'+ #receivable.no +'】的'}}回款: {_DIFF{#updateReqVO}}";
     String CRM_RECEIVABLE_DELETE_SUB_TYPE = "删除回款";
-    String CRM_RECEIVABLE_DELETE_SUCCESS = "删除了合同【{getContractById{#receivable.contractId}}】的第【{{#receivable.period}}】期回款";
+    String CRM_RECEIVABLE_DELETE_SUCCESS = "删除了合同【{getContractById{#receivable.contractId}}】的{{#period != null ? '【第'+ #period +'期】' : '编号为【'+ #receivable.no +'】的'}}回款";
     String CRM_RECEIVABLE_SUBMIT_SUB_TYPE = "提交回款审批";
     String CRM_RECEIVABLE_SUBMIT_SUCCESS = "提交编号为【{{#receivableNo}}】的回款审批成功";
 

+ 7 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionLevelEnum.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.enums.permission;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.AllArgsConstructor;
@@ -50,4 +51,10 @@ public enum CrmPermissionLevelEnum implements IntArrayValuable {
         return ObjUtil.equal(WRITE.level, level);
     }
 
+    public static String getNameByLevel(Integer level) {
+        CrmPermissionLevelEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(CrmPermissionLevelEnum.values()),
+                item -> ObjUtil.equal(item.level, level));
+        return typeEnum == null ? null : typeEnum.getName();
+    }
+
 }

+ 2 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessSaveReqVO.java

@@ -66,13 +66,13 @@ public class CrmBusinessSaveReqVO {
     private Long contactId; // 使用场景,在【联系人详情】添加商机时,如果需要关联两者,需要传递 contactId 字段
 
     @Schema(description = "产品列表")
-    private List<Product> products;
+    private List<BusinessProduct> businessProducts;
 
     @Schema(description = "产品列表")
     @Data
     @NoArgsConstructor
     @AllArgsConstructor
-    public static class Product {
+    public static class BusinessProduct {
 
         @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
         @NotNull(message = "产品编号不能为空")

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessTransferReqVO.java

@@ -3,10 +3,14 @@ package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 @Schema(description = "管理后台 - 商机转移 Request VO")
 @Data
+@NoArgsConstructor
+@AllArgsConstructor
 public class CrmBusinessTransferReqVO {
 
     @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java

@@ -2,12 +2,16 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
 
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
 import lombok.Data;
 
 import jakarta.validation.constraints.NotNull;
+import lombok.NoArgsConstructor;
 
 @Schema(description = "管理后台 - CRM 联系人转移 Request VO")
 @Data
+@NoArgsConstructor
+@AllArgsConstructor
 public class CrmContactTransferReqVO {
 
     @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractTransferReqVO.java

@@ -3,12 +3,16 @@ package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract;
 import cn.iocoder.yudao.framework.common.validation.InEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
 import lombok.Data;
 
 import jakarta.validation.constraints.NotNull;
+import lombok.NoArgsConstructor;
 
 @Schema(description = "管理后台 - CRM 合同转移 Request VO")
 @Data
+@NoArgsConstructor
+@AllArgsConstructor
 public class CrmContractTransferReqVO {
 
     @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")

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

@@ -11,9 +11,7 @@ import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.framework.excel.core.enums.ExcelColumn;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-import cn.iocoder.yudao.framework.ip.core.Area;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
@@ -38,7 +36,6 @@ import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
 import java.time.LocalDateTime;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -49,7 +46,6 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.*;
 import static java.util.Collections.singletonList;
 
 @Tag(name = "管理后台 - CRM 客户")
@@ -67,8 +63,6 @@ public class CrmCustomerController {
     private DeptApi deptApi;
     @Resource
     private AdminUserApi adminUserApi;
-    @Resource
-    private DictDataApi dictDataApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建客户")
@@ -142,7 +136,7 @@ public class CrmCustomerController {
             return java.util.Collections.emptyList();
         }
         // 1.1 获取创建人、负责人列表
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(list,
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSetByFlatMap(list,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
         // 1.2 获取距离进入公海的时间
@@ -265,25 +259,7 @@ public class CrmCustomerController {
                         .areaId(null).detailAddress("").remark("").build()
         );
         // 输出
-        ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list, builderSelectMap());
-    }
-
-    private List<KeyValue<ExcelColumn, List<String>>> builderSelectMap() {
-        List<KeyValue<ExcelColumn, List<String>>> selectMap = new ArrayList<>();
-        // 获取地区下拉数据
-        // TODO @puhui999:嘿嘿,这里改成省份、城市、区域,三个选项,难度大么?
-        Area area = AreaUtils.getArea(Area.ID_CHINA);
-        selectMap.add(new KeyValue<>(ExcelColumn.G, AreaUtils.getAreaNodePathList(area.getChildren())));
-        // 获取客户所属行业
-        List<String> customerIndustries = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_INDUSTRY);
-        selectMap.add(new KeyValue<>(ExcelColumn.I, customerIndustries));
-        // 获取客户等级
-        List<String> customerLevels = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_LEVEL);
-        selectMap.add(new KeyValue<>(ExcelColumn.J, customerLevels));
-        // 获取客户来源
-        List<String> customerSources = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_SOURCE);
-        selectMap.add(new KeyValue<>(ExcelColumn.K, customerSources));
-        return selectMap;
+        ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list);
     }
 
     @PostMapping("/import")

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

@@ -1,8 +1,10 @@
 package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
 
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
 import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert;
 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.crm.framework.excel.core.AreaExcelColumnSelectFunction;
 import com.alibaba.excel.annotation.ExcelProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -41,6 +43,7 @@ public class CrmCustomerImportExcelVO {
     private String email;
 
     @ExcelProperty(value = "地区", converter = AreaConvert.class)
+    @ExcelColumnSelect(functionName = AreaExcelColumnSelectFunction.NAME)
     private Integer areaId;
 
     @ExcelProperty("详细地址")
@@ -48,14 +51,17 @@ public class CrmCustomerImportExcelVO {
 
     @ExcelProperty(value = "所属行业", converter = DictConvert.class)
     @DictFormat(CRM_CUSTOMER_INDUSTRY)
+    @ExcelColumnSelect(dictType = CRM_CUSTOMER_INDUSTRY)
     private Integer industryId;
 
     @ExcelProperty(value = "客户等级", converter = DictConvert.class)
     @DictFormat(CRM_CUSTOMER_LEVEL)
+    @ExcelColumnSelect(dictType = CRM_CUSTOMER_LEVEL)
     private Integer level;
 
     @ExcelProperty(value = "客户来源", converter = DictConvert.class)
     @DictFormat(CRM_CUSTOMER_SOURCE)
+    @ExcelColumnSelect(dictType = CRM_CUSTOMER_SOURCE)
     private Integer source;
 
     @ExcelProperty("备注")

+ 8 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerTransferReqVO.java

@@ -5,6 +5,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
+import java.util.List;
+
 @Schema(description = "管理后台 - CRM 客户转移 Request VO")
 @Data
 public class CrmCustomerTransferReqVO {
@@ -28,4 +30,10 @@ public class CrmCustomerTransferReqVO {
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     private Integer oldOwnerPermissionLevel;
 
+    /**
+     * 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择。选中时,也一起转移
+     */
+    @Schema(description = "同时转移", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    private List<Integer> toBizTypes;
+
 }

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

@@ -1,16 +1,24 @@
 package cn.iocoder.yudao.module.crm.controller.admin.permission;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-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.CrmPermissionSaveReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
+import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -27,13 +35,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -50,7 +56,6 @@ public class CrmPermissionController {
 
     @Resource
     private CrmPermissionService permissionService;
-
     @Resource
     private AdminUserApi adminUserApi;
     @Resource
@@ -61,9 +66,8 @@ public class CrmPermissionController {
     @PostMapping("/create")
     @Operation(summary = "创建数据权限")
     @PreAuthorize("@ss.hasPermission('crm:permission:create')")
-    @CrmPermission(bizTypeValue = "#reqVO.bizType", bizId = "#reqVO.bizId", level = CrmPermissionLevelEnum.OWNER)
-    public CommonResult<Boolean> addPermission(@Valid @RequestBody CrmPermissionCreateReqVO reqVO) {
-        permissionService.createPermission(BeanUtils.toBean(reqVO, CrmPermissionCreateReqBO.class));
+    public CommonResult<Boolean> create(@Valid @RequestBody CrmPermissionSaveReqVO reqVO) {
+        permissionService.createPermission(reqVO, getLoginUserId());
         return success(true);
     }
 

+ 0 - 14
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionCreateReqVO.java

@@ -1,14 +0,0 @@
-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 {
-
-}

+ 23 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionRespVO.java

@@ -1,6 +1,10 @@
 package cn.iocoder.yudao.module.crm.controller.admin.permission.vo;
 
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -8,11 +12,29 @@ import java.util.Set;
 
 @Schema(description = "管理后台 - CRM 数据权限 Response VO")
 @Data
-public class CrmPermissionRespVO extends CrmPermissionBaseVO {
+public class CrmPermissionRespVO {
 
     @Schema(description = "数据权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     private Long id;
 
+    @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;
+
     @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
     private String nickname;
 

+ 11 - 7
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionBaseVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/vo/CrmPermissionSaveReqVO.java

@@ -8,14 +8,11 @@ import lombok.Data;
 
 import jakarta.validation.constraints.NotNull;
 
-/**
- * 数据权限 Base VO,提供给添加、修改、详细的子 VO 使用
- * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
- *
- * @author HUIHUI
- */
+import java.util.List;
+
+@Schema(description = "管理后台 - CRM 数据权限创建/更新 Request VO")
 @Data
-public class CrmPermissionBaseVO {
+public class CrmPermissionSaveReqVO {
 
     @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
     @NotNull(message = "用户编号不能为空")
@@ -35,4 +32,11 @@ public class CrmPermissionBaseVO {
     @NotNull(message = "权限级别不能为空")
     private Integer level;
 
+    /**
+     * 添加客户团队成员时,需要额外有【联系人】【商机】【合同】的 checkbox 选择。
+     * 选中时,同时添加对应的权限
+     */
+    @Schema(description = "同时添加", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    private List<Integer> toBizTypes;
+
 }

+ 18 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan;
 
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
@@ -8,53 +9,69 @@ import lombok.Data;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
-// TODO @puhui999:缺导出
 @Schema(description = "管理后台 - CRM 回款计划 Response VO")
 @Data
+@ExcelIgnoreUnannotated
 public class CrmReceivablePlanRespVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("编号")
     private Long id;
 
     @Schema(description = "期数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("期数")
     private Integer period;
 
     @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("客户编号")
     private Long customerId;
     @Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
+    @ExcelProperty("客户名字")
     private String customerName;
 
     @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("合同编号")
     private Long contractId;
     @Schema(description = "合同编号", example = "Q110")
+    @ExcelProperty("合同编号")
     private String contractNo;
 
     @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("负责人编号")
     private Long ownerUserId;
     @Schema(description = "负责人", example = "test")
+    @ExcelProperty("负责人")
     private String ownerUserName;
 
     @Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    @ExcelProperty("计划回款日期")
     private LocalDateTime returnTime;
 
     @Schema(description = "计划回款方式", example = "1")
+    @ExcelProperty("计划回款方式")
     private Integer returnType;
 
     @Schema(description = "计划回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+    @ExcelProperty("计划回款金额")
     private BigDecimal price;
 
     @Schema(description = "回款编号", example = "19852")
+    @ExcelProperty("回款编号")
     private Long receivableId;
     @Schema(description = "回款信息")
+    @ExcelProperty("回款信息")
     private CrmReceivableRespVO receivable;
 
     @Schema(description = "提前几天提醒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("提前几天提醒")
     private Integer remindDays;
 
     @Schema(description = "提醒日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    @ExcelProperty("提醒日期")
     private LocalDateTime remindTime;
 
     @Schema(description = "备注", example = "备注")
+    @ExcelProperty("备注")
     private String remark;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)

+ 19 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable;
 
 import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
@@ -8,37 +9,47 @@ import lombok.Data;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
-// TODO 芋艿:导出的 VO,可以考虑使用 @Excel 注解,实现导出功能
 @Schema(description = "管理后台 - CRM 回款 Response VO")
 @Data
+@ExcelIgnoreUnannotated
 public class CrmReceivableRespVO {
 
     @Schema(description = "编号", example = "25787")
+    @ExcelProperty("编号")
     private Long id;
 
     @Schema(description = "回款编号", example = "31177")
+    @ExcelProperty("回款编号")
     private String no;
 
     @Schema(description = "回款计划编号", example = "1024")
+    @ExcelProperty("回款计划编号")
     private Long planId;
 
     @Schema(description = "回款方式", example = "2")
+    @ExcelProperty("回款方式")
     private Integer returnType;
 
     @Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+    @ExcelProperty("回款金额")
     private BigDecimal price;
 
     @Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    @ExcelProperty("计划回款日期")
     private LocalDateTime returnTime;
 
     @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("客户编号")
     private Long customerId;
     @Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
+    @ExcelProperty("客户名字")
     private String customerName;
 
     @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("合同编号")
     private Long contractId;
     @Schema(description = "合同信息")
+    @ExcelProperty("合同信息")
     private CrmContractRespVO contract;
 
     @Schema(description = "负责人的用户编号", example = "25682")
@@ -56,20 +67,26 @@ public class CrmReceivableRespVO {
     private String processInstanceId;
 
     @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    @ExcelProperty("审批状态")
     private Integer auditStatus;
 
-    @Schema(description = "备注", example = "备注")
+    @Schema(description = "工作流编号", example = "备注")
+    @ExcelProperty("工作流编号")
     private String remark;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
     private LocalDateTime createTime;
 
     @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("更新时间")
     private LocalDateTime updateTime;
 
     @Schema(description = "创建人", example = "25682")
+    @ExcelProperty("创建人")
     private String creator;
     @Schema(description = "创建人名字", example = "test")
+    @ExcelProperty("创建人名字")
     private String creatorName;
 
 }

+ 55 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http

@@ -0,0 +1,55 @@
+# == 1. 客户总量分析 ==
+### 1.1 客户总量分析(按日期)
+GET {{baseUrl}}/crm/statistics-customer/get-customer-summary-by-date?deptId=100&interval=2&times[0]=2024-01-01 00:00:00&times[1]=2024-01-29 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 1.2 客户总量统计(按用户)
+GET {{baseUrl}}/crm/statistics-customer/get-customer-summary-by-user?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+# == 2. 客户跟进次数分析 ==
+### 2.1 客户跟进次数分析(按日期)
+GET {{baseUrl}}/crm/statistics-customer/get-follow-up-summary-by-date?deptId=100&interval=2&times[0]=2024-01-01 00:00:00&times[1]=2024-01-29 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 2.2 客户总量统计(按用户)
+GET {{baseUrl}}/crm/statistics-customer/get-follow-up-summary-by-user?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+# == 3. 客户跟进方式分析 ==
+### 3.1 客户跟进方式分析
+GET {{baseUrl}}/crm/statistics-customer/get-follow-up-summary-by-type?deptId=100&interval=2&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+# == 4. 客户成交周期 ==
+### 4.1 合同摘要信息(客户转化率页面)
+GET {{baseUrl}}/crm/statistics-customer/get-contract-summary?deptId=100&interval=2&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+# == 5. 客户成交周期 ==
+### 5.1 获取客户公海分析(按日期)
+GET {{baseUrl}}/crm/statistics-customer/get-pool-summary-by-date?deptId=100&interval=2&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 5.2 获取客户公海分析(按用户)
+GET {{baseUrl}}/crm/statistics-customer/get-pool-summary-by-user?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+# == 6. 客户成交周期 ==
+### 6.1 客户成交周期(按日期)
+GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-date?deptId=100&interval=2&times[0]=2024-01-01 00:00:00&times[1]=2024-01-29 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 6.2 获取客户成交周期(按用户)
+GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-user?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

+ 101 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.java

@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
+import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsCustomerService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+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 java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - CRM 客户统计")
+@RestController
+@RequestMapping("/crm/statistics-customer")
+@Validated
+public class CrmStatisticsCustomerController {
+
+    @Resource
+    private CrmStatisticsCustomerService customerService;
+
+    @GetMapping("/get-customer-summary-by-date")
+    @Operation(summary = "获取客户总量分析(按日期)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsCustomerSummaryByDateRespVO>> getCustomerSummaryByDate(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getCustomerSummaryByDate(reqVO));
+    }
+
+    @GetMapping("/get-customer-summary-by-user")
+    @Operation(summary = "获取客户总量分析(按用户)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsCustomerSummaryByUserRespVO>> getCustomerSummaryByUser(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getCustomerSummaryByUser(reqVO));
+    }
+
+    @GetMapping("/get-follow-up-summary-by-date")
+    @Operation(summary = "获取客户跟进次数分析(按日期)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsFollowUpSummaryByDateRespVO>> getFollowupSummaryByDate(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getFollowUpSummaryByDate(reqVO));
+    }
+
+    @GetMapping("/get-follow-up-summary-by-user")
+    @Operation(summary = "获取客户跟进次数分析(按用户)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsFollowUpSummaryByUserRespVO>> getFollowUpSummaryByUser(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getFollowUpSummaryByUser(reqVO));
+    }
+
+    @GetMapping("/get-follow-up-summary-by-type")
+    @Operation(summary = "获取客户跟进次数分析(按类型)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsFollowUpSummaryByTypeRespVO>> getFollowUpSummaryByType(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getFollowUpSummaryByType(reqVO));
+    }
+
+    @GetMapping("/get-contract-summary")
+    @Operation(summary = "获取客户的首次合同、回款信息列表", description = "用于【客户转化率】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsCustomerContractSummaryRespVO>> getContractSummary(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getContractSummary(reqVO));
+    }
+
+    @GetMapping("/get-pool-summary-by-date")
+    @Operation(summary = "获取公海客户分析(按日期)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsPoolSummaryByDateRespVO>> getPoolSummaryByDate(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getPoolSummaryByDate(reqVO));
+    }
+
+    @GetMapping("/get-pool-summary-by-user")
+    @Operation(summary = "获取公海客户分析(按用户)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsPoolSummaryByUserRespVO>> getPoolSummaryByUser(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getPoolSummaryByUser(reqVO));
+    }
+
+    @GetMapping("/get-customer-deal-cycle-by-date")
+    @Operation(summary = "获取客户成交周期(按日期)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsCustomerDealCycleByDateRespVO>> getCustomerDealCycleByDate(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getCustomerDealCycleByDate(reqVO));
+    }
+
+    @GetMapping("/get-customer-deal-cycle-by-user")
+    @Operation(summary = "获取客户成交周期(按用户)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsCustomerDealCycleByUserRespVO>> getCustomerDealCycleByUser(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getCustomerDealCycleByUser(reqVO));
+    }
+
+    // TODO dhb52:【成交周期分析】里,有按照员工(已实现)、地区(未实现)、产品(未实现),需要在看看哈;可以把 CustomerDealCycle 拆成 3 个 tab,员工客户成交周期分析、地区客户成交周期分析、产品客户成交周期分析;
+
+}

+ 52 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsPerformanceController.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
+import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsPerformanceService;
+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 jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+
+@Tag(name = "管理后台 - CRM 员工业绩统计")
+@RestController
+@RequestMapping("/crm/statistics-performance")
+@Validated
+public class CrmStatisticsPerformanceController {
+
+    @Resource
+    private CrmStatisticsPerformanceService performanceService;
+
+    @GetMapping("/get-contract-count-performance")
+    @Operation(summary = "合同数量统计", description = "用于【合同数量分析】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-performance:query')")
+    public CommonResult<List<CrmStatisticsPerformanceRespVO>> getContractCountPerformance(@Valid CrmStatisticsPerformanceReqVO performanceReqVO) {
+        return success(performanceService.getContractCountPerformance(performanceReqVO));
+    }
+
+    @GetMapping("/get-contract-price-performance")
+    @Operation(summary = "合同金额统计")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-performance:query')")
+    public CommonResult<List<CrmStatisticsPerformanceRespVO>> getContractPriceStaffPerformance(@Valid CrmStatisticsPerformanceReqVO performanceReqVO) {
+        return success(performanceService.getContractPricePerformance(performanceReqVO));
+    }
+
+    @GetMapping("/get-receivable-price-performance")
+    @Operation(summary = "回款金额统计")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-performance:query')")
+    public CommonResult<List<CrmStatisticsPerformanceRespVO>> getReceivablePriceStaffPerformance(@Valid CrmStatisticsPerformanceReqVO performanceReqVO) {
+        return success(performanceService.getReceivablePricePerformance(performanceReqVO));
+    }
+
+}

+ 61 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsPortraitController.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticsPortraitReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticCustomerAreaRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticCustomerIndustryRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticCustomerLevelRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticCustomerSourceRespVO;
+import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsPortraitService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+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 java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - CRM 客户画像")
+@RestController
+@RequestMapping("/crm/statistics-portrait")
+@Validated
+public class CrmStatisticsPortraitController {
+
+    @Resource
+    private CrmStatisticsPortraitService statisticsPortraitService;
+
+    @GetMapping("/get-customer-area-summary")
+    @Operation(summary = "获取客户地区统计数据", description = "用于【城市分布分析】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-portrait:query')")
+    public CommonResult<List<CrmStatisticCustomerAreaRespVO>> getCustomerAreaSummary(@Valid CrmStatisticsPortraitReqVO reqVO) {
+        return success(statisticsPortraitService.getCustomerSummaryByArea(reqVO));
+    }
+
+    @GetMapping("/get-customer-industry-summary")
+    @Operation(summary = "获取客户行业统计数据", description = "用于【客户行业分析】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-portrait:query')")
+    public CommonResult<List<CrmStatisticCustomerIndustryRespVO>> getCustomerIndustrySummary(@Valid CrmStatisticsPortraitReqVO reqVO) {
+        return success(statisticsPortraitService.getCustomerSummaryByIndustry(reqVO));
+    }
+
+    @GetMapping("/get-customer-level-summary")
+    @Operation(summary = "获取客户级别统计数据", description = "用于【客户级别分析】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-portrait:query')")
+    public CommonResult<List<CrmStatisticCustomerLevelRespVO>> getCustomerLevelSummary(@Valid CrmStatisticsPortraitReqVO reqVO) {
+        return success(statisticsPortraitService.getCustomerSummaryByLevel(reqVO));
+    }
+
+    @GetMapping("/get-customer-source-summary")
+    @Operation(summary = "获取客户来源统计数据", description = "用于【客户来源分析】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-portrait:query')")
+    public CommonResult<List<CrmStatisticCustomerSourceRespVO>> getCustomerSourceSummary(@Valid CrmStatisticsPortraitReqVO reqVO) {
+        return success(statisticsPortraitService.getCustomerSummaryBySource(reqVO));
+    }
+
+}

+ 20 - 21
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsRankController.java

@@ -1,9 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.statistics;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRankReqVO;
-import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsRankingService;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankReqVO;
+import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsRankService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
@@ -18,7 +18,6 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
-
 @Tag(name = "管理后台 - CRM 排行榜统计")
 @RestController
 @RequestMapping("/crm/statistics-rank")
@@ -26,62 +25,62 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 public class CrmStatisticsRankController {
 
     @Resource
-    private CrmStatisticsRankingService rankingService;
+    private CrmStatisticsRankService rankService;
 
     @GetMapping("/get-contract-price-rank")
     @Operation(summary = "获得合同金额排行榜")
     @PreAuthorize("@ss.hasPermission('crm:statistics-rank:query')")
-    public CommonResult<List<CrmStatisticsRanKRespVO>> getContractPriceRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
-        return success(rankingService.getContractPriceRank(rankingReqVO));
+    public CommonResult<List<CrmStatisticsRankRespVO>> getContractPriceRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
+        return success(rankService.getContractPriceRank(rankingReqVO));
     }
 
     @GetMapping("/get-receivable-price-rank")
     @Operation(summary = "获得回款金额排行榜")
     @PreAuthorize("@ss.hasPermission('crm:statistics-rank:query')")
-    public CommonResult<List<CrmStatisticsRanKRespVO>> getReceivablePriceRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
-        return success(rankingService.getReceivablePriceRank(rankingReqVO));
+    public CommonResult<List<CrmStatisticsRankRespVO>> getReceivablePriceRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
+        return success(rankService.getReceivablePriceRank(rankingReqVO));
     }
 
     @GetMapping("/get-contract-count-rank")
     @Operation(summary = "获得签约合同数量排行榜")
     @PreAuthorize("@ss.hasPermission('crm:statistics-rank:query')")
-    public CommonResult<List<CrmStatisticsRanKRespVO>> getContractCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
-        return success(rankingService.getContractCountRank(rankingReqVO));
+    public CommonResult<List<CrmStatisticsRankRespVO>> getContractCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
+        return success(rankService.getContractCountRank(rankingReqVO));
     }
 
     @GetMapping("/get-product-sales-rank")
     @Operation(summary = "获得产品销量排行榜")
     @PreAuthorize("@ss.hasPermission('crm:statistics-rank:query')")
-    public CommonResult<List<CrmStatisticsRanKRespVO>> getProductSalesRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
-        return success(rankingService.getProductSalesRank(rankingReqVO));
+    public CommonResult<List<CrmStatisticsRankRespVO>> getProductSalesRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
+        return success(rankService.getProductSalesRank(rankingReqVO));
     }
 
     @GetMapping("/get-customer-count-rank")
     @Operation(summary = "获得新增客户数排行榜")
     @PreAuthorize("@ss.hasPermission('crm:statistics-rank:query')")
-    public CommonResult<List<CrmStatisticsRanKRespVO>> getCustomerCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
-        return success(rankingService.getCustomerCountRank(rankingReqVO));
+    public CommonResult<List<CrmStatisticsRankRespVO>> getCustomerCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
+        return success(rankService.getCustomerCountRank(rankingReqVO));
     }
 
     @GetMapping("/get-contacts-count-rank")
     @Operation(summary = "获得新增联系人数排行榜")
     @PreAuthorize("@ss.hasPermission('crm:statistics-rank:query')")
-    public CommonResult<List<CrmStatisticsRanKRespVO>> getContactsCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
-        return success(rankingService.getContactsCountRank(rankingReqVO));
+    public CommonResult<List<CrmStatisticsRankRespVO>> getContactsCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
+        return success(rankService.getContactsCountRank(rankingReqVO));
     }
 
     @GetMapping("/get-follow-count-rank")
     @Operation(summary = "获得跟进次数排行榜")
     @PreAuthorize("@ss.hasPermission('crm:statistics-rank:query')")
-    public CommonResult<List<CrmStatisticsRanKRespVO>> getFollowCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
-        return success(rankingService.getFollowCountRank(rankingReqVO));
+    public CommonResult<List<CrmStatisticsRankRespVO>> getFollowCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
+        return success(rankService.getFollowCountRank(rankingReqVO));
     }
 
     @GetMapping("/get-follow-customer-count-rank")
     @Operation(summary = "获得跟进客户数排行榜")
     @PreAuthorize("@ss.hasPermission('crm:statistics-rank:query')")
-    public CommonResult<List<CrmStatisticsRanKRespVO>> getFollowCustomerCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
-        return success(rankingService.getFollowCustomerCountRank(rankingReqVO));
+    public CommonResult<List<CrmStatisticsRankRespVO>> getFollowCustomerCountRank(@Valid CrmStatisticsRankReqVO rankingReqVO) {
+        return success(rankService.getFollowCustomerCountRank(rankingReqVO));
     }
 
 }

+ 20 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerByUserBaseRespVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 用户客户统计响应 Base Response VO
+ *
+ * 目的:可以统一拼接子 VO 的 ownerUserId、ownerUserName 属性
+ */
+@Data
+public class CrmStatisticsCustomerByUserBaseRespVO {
+
+    @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long ownerUserId;
+
+    @Schema(description = "负责人", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
+    private String ownerUserName;
+
+}

+ 48 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerContractSummaryRespVO.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - CRM 客户转化率分析 VO")
+@Data
+public class CrmStatisticsCustomerContractSummaryRespVO {
+
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
+    private String customerName;
+
+    @Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "演示合同")
+    private String contractName;
+
+    @Schema(description = "合同总金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1200.00")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1200.00")
+    private BigDecimal receivablePrice;
+
+    @Schema(description = "客户行业编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer industryId;
+
+    @Schema(description = "客户来源编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer source;
+
+    @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long ownerUserId;
+    @Schema(description = "负责人", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
+    private String ownerUserName;
+
+    @Schema(description = "创建人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private String creator;
+    @Schema(description = "创建人", requiredMode = Schema.RequiredMode.REQUIRED, example = "源码")
+    private String creatorUserName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-01 13:24:26")
+    private LocalDateTime createTime;
+
+    @Schema(description = "下单日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02 00:00:00")
+    private LocalDateTime orderDate;
+
+}

+ 16 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByDateRespVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户成交周期分析(按日期) VO")
+@Data
+public class CrmStatisticsCustomerDealCycleByDateRespVO {
+
+    @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+    private String time;
+
+    @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
+    private Double customerDealCycle;
+
+}

+ 16 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByUserRespVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 成交周期分析(按用户) VO")
+@Data
+public class CrmStatisticsCustomerDealCycleByUserRespVO extends CrmStatisticsCustomerByUserBaseRespVO {
+
+    @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
+    private Double customerDealCycle;
+
+    @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerDealCount;
+
+}

+ 46 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerReqVO.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - CRM 数据统计的员工客户分析 Request VO")
+@Data
+public class CrmStatisticsCustomerReqVO {
+
+    @Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "部门 id 不能为空")
+    private Long deptId;
+
+    /**
+     * 负责人用户 id, 当用户为空, 则计算部门下用户
+     */
+    @Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
+    private Long userId;
+
+    /**
+     * userIds 目前不用前端传递,目前是方便后端通过 deptId 读取编号后,设置回来
+     * 后续,可能会支持选择部分用户进行查询
+     */
+    @Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
+    private List<Long> userIds;
+
+    @Schema(description = "时间间隔类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @InEnum(value = DateIntervalEnum.class, message = "时间间隔类型,必须是 {value}")
+    private Integer interval;
+
+    @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @Size(min = 2, max = 2, message = "请选择时间范围")
+    private LocalDateTime[] times;
+
+}

+ 19 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerSummaryByDateRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户总量分析(按日期) VO")
+@Data
+public class CrmStatisticsCustomerSummaryByDateRespVO {
+
+    @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+    private String time;
+
+    @Schema(description = "新建客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerCreateCount;
+
+    @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerDealCount;
+
+}

+ 24 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerSummaryByUserRespVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - CRM 客户总量分析(按用户) VO")
+@Data
+public class CrmStatisticsCustomerSummaryByUserRespVO extends CrmStatisticsCustomerByUserBaseRespVO {
+
+    @Schema(description = "新建客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerCreateCount;
+
+    @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerDealCount;
+
+    @Schema(description = "合同总金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+    private BigDecimal contractPrice;
+
+    @Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+    private BigDecimal receivablePrice;
+
+}

+ 19 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsFollowUpSummaryByDateRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 跟进次数分析(按日期) VO")
+@Data
+public class CrmStatisticsFollowUpSummaryByDateRespVO {
+
+    @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+    private String time;
+
+    @Schema(description = "跟进次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer followUpRecordCount;
+
+    @Schema(description = "跟进客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer followUpCustomerCount;
+
+}

+ 17 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsFollowUpSummaryByTypeRespVO.java

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 跟进次数分析(按类型) VO")
+@Data
+public class CrmStatisticsFollowUpSummaryByTypeRespVO {
+
+    @Schema(description = "跟进类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer followUpType;
+
+    @Schema(description = "跟进次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer followUpRecordCount;
+
+}

+ 16 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsFollowUpSummaryByUserRespVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 跟进次数分析(按用户) VO")
+@Data
+public class CrmStatisticsFollowUpSummaryByUserRespVO extends CrmStatisticsCustomerByUserBaseRespVO {
+
+    @Schema(description = "跟进次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer followUpRecordCount;
+
+    @Schema(description = "跟进客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer followUpCustomerCount;
+
+}

+ 19 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsPoolSummaryByDateRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 公海客户分析(按日期) VO")
+@Data
+public class CrmStatisticsPoolSummaryByDateRespVO {
+
+    @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+    private String time;
+
+    @Schema(description = "进入公海客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerPutCount;
+
+    @Schema(description = "公海领取客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerTakeCount;
+
+}

+ 16 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsPoolSummaryByUserRespVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 公海客户分析(按用户) VO")
+@Data
+public class CrmStatisticsPoolSummaryByUserRespVO extends CrmStatisticsCustomerByUserBaseRespVO {
+
+    @Schema(description = "进入公海客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerPutCount;
+
+    @Schema(description = "公海领取客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerTakeCount;
+
+}

+ 42 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/performance/CrmStatisticsPerformanceReqVO.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - CRM 员工业绩统计 Request VO")
+@Data
+public class CrmStatisticsPerformanceReqVO {
+
+    @Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "部门 id 不能为空")
+    private Long deptId;
+
+    /**
+     * 负责人用户 id, 当用户为空, 则计算部门下用户
+     */
+    @Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
+    private Long userId;
+
+    /**
+     * userIds 目前不用前端传递,目前是方便后端通过 deptId 读取编号后,设置回来
+     * <p>
+     * 后续,可能会支持选择部分用户进行查询
+     */
+    @Schema(description = "负责人用户 id 集合", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2")
+    private List<Long> userIds;
+
+    // TODO @scholar:应该传递的是 int year;年份
+    @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.REQUIRED)
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @NotEmpty(message = "时间范围不能为空")
+    private LocalDateTime[] times;
+
+}

+ 24 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/performance/CrmStatisticsPerformanceRespVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import java.math.BigDecimal;
+
+
+@Schema(description = "管理后台 - CRM 员工业绩统计 Response VO")
+@Data
+public class CrmStatisticsPerformanceRespVO {
+
+    @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+    private String time;
+
+    @Schema(description = "当月统计结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private BigDecimal currentMonthCount;
+
+    @Schema(description = "上月统计结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private BigDecimal lastMonthCount;
+
+    @Schema(description = "去年同期统计结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
+    private BigDecimal lastYearCount;
+
+}

+ 21 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticCustomerAreaRespVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户省份分析 VO")
+@Data
+public class CrmStatisticCustomerAreaRespVO {
+
+    @Schema(description = "省份编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer areaId;
+    @Schema(description = "省份名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "浙江省")
+    private String areaName;
+
+    @Schema(description = "客户个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerCount;
+
+    @Schema(description = "成交个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer dealCount;
+
+}

+ 19 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticCustomerIndustryRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户行业分析 VO")
+@Data
+public class CrmStatisticCustomerIndustryRespVO {
+
+    @Schema(description = "客户行业ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer industryId;
+
+    @Schema(description = "客户个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerCount;
+
+    @Schema(description = "成交个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer dealCount;
+
+}

+ 19 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticCustomerLevelRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户级别分析 VO")
+@Data
+public class CrmStatisticCustomerLevelRespVO {
+
+    @Schema(description = "客户级别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer level;
+
+    @Schema(description = "客户个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerCount;
+
+    @Schema(description = "成交个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer dealCount;
+
+}

+ 19 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticCustomerSourceRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户来源分析 VO")
+@Data
+public class CrmStatisticCustomerSourceRespVO {
+
+    @Schema(description = "客户来源编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer source;
+
+    @Schema(description = "客户个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerCount;
+
+    @Schema(description = "成交个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer dealCount;
+
+}

+ 42 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticsPortraitReqVO.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - CRM 客户画像 Request VO")
+@Data
+public class CrmStatisticsPortraitReqVO {
+
+    @Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "部门 id 不能为空")
+    private Long deptId;
+
+    /**
+     * 负责人用户 id, 当用户为空, 则计算部门下用户
+     */
+    @Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
+    private Long userId;
+
+    /**
+     * userIds 目前不用前端传递,目前是方便后端通过 deptId 读取编号后,设置回来
+     * 后续,可能会支持选择部分用户进行查询
+     */
+    @Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
+    private List<Long> userIds;
+
+    /**
+     * 前端如果选择自定义时间, 那么前端传递起始-终止时间, 如果选择其他时间间隔类型, 则由后台计算起始-终止时间
+     * 并作为参数传递给Mapper
+     */
+    @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] times;
+
+}

+ 2 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/CrmStatisticsRankReqVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/rank/CrmStatisticsRankReqVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotEmpty;
@@ -21,7 +21,7 @@ public class CrmStatisticsRankReqVO {
 
     /**
      * userIds 目前不用前端传递,目前是方便后端通过 deptId 读取编号后,设置回来
-     *
+     * <p>
      * 后续,可能会支持选择部分用户进行查询
      */
     @Schema(description = "负责人用户 id 集合", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2")

+ 8 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/CrmStatisticsRanKRespVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/rank/CrmStatisticsRankRespVO.java

@@ -1,12 +1,14 @@
-package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.math.BigDecimal;
 
-@Schema(description = "管理后台 - CRM BI 排行榜统计 Response VO")
+
+@Schema(description = "管理后台 - CRM 排行榜统计 Response VO")
 @Data
-public class CrmStatisticsRanKRespVO {
+public class CrmStatisticsRankRespVO {
 
     @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Long ownerUserId;
@@ -22,8 +24,10 @@ public class CrmStatisticsRanKRespVO {
      *
      * 1. 金额:合同金额排行、回款金额排行
      * 2. 个数:签约合同排行、产品销量排行、产品销量排行、新增客户数排行、新增联系人排行、跟进次数排行、跟进客户数排行
+     *
+     * 为什么使用 BigDecimal 的原因:
      */
     @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    private Integer count;
+    private BigDecimal count;
 
 }

+ 8 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java

@@ -6,12 +6,14 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessPageReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * 商机 Mapper
@@ -57,4 +59,10 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
         return selectCount(CrmBusinessDO::getStatusTypeId, statusTypeId);
     }
 
+    default List<CrmBusinessDO> selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId){
+        return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
+                .eq(CrmBusinessDO::getCustomerId, customerId)
+                .eq(CrmBusinessDO::getOwnerUserId, ownerUserId));
+    }
+
 }

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

@@ -73,4 +73,9 @@ public interface CrmContactMapper extends BaseMapperX<CrmContactDO> {
         return selectList(CrmContactDO::getCustomerId, customerId);
     }
 
+    default List<CrmContactDO> selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
+        return selectList(CrmContactDO::getCustomerId, customerId,
+                CrmContactDO::getOwnerUserId, ownerUserId);
+    }
+
 }

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

@@ -117,4 +117,10 @@ public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
         return selectCount(query);
     }
 
+    default List<CrmContractDO> selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
+        return selectList(new LambdaQueryWrapperX<CrmContractDO>()
+                .eq(CrmContractDO::getCustomerId, customerId)
+                .eq(CrmContractDO::getOwnerUserId, ownerUserId));
+    }
+
 }

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

@@ -53,9 +53,11 @@ public interface CrmPermissionMapper extends BaseMapperX<CrmPermissionDO> {
                 CrmPermissionDO::getUserId, userId);
     }
 
-    default CrmPermissionDO selectByBizIdAndUserId(Long bizId, Long userId) {
-        return selectOne(CrmPermissionDO::getBizId, bizId,
-                CrmPermissionDO::getUserId, userId);
+    default CrmPermissionDO selectByBizAndUserId(Integer bizType, Long bizId, Long userId) {
+        return selectOne(new LambdaQueryWrapperX<CrmPermissionDO>()
+                .eq(CrmPermissionDO::getBizType, bizType)
+                .eq(CrmPermissionDO::getBizId, bizId)
+                .eq(CrmPermissionDO::getUserId, userId));
     }
 
     default int deletePermission(Integer bizType, Long bizId) {

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java

@@ -99,4 +99,8 @@ public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
         return convertMap(result, obj -> (Long) obj.get("contract_id"), obj -> (BigDecimal) obj.get("total_price"));
     }
 
+    default Long selectCountByContractId(Long contractId) {
+        return selectCount(CrmReceivableDO::getContractId, contractId);
+    }
+
 }

+ 194 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java

@@ -0,0 +1,194 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * CRM 客户分析 Mapper
+ *
+ * @author dhb52
+ */
+@Mapper
+public interface CrmStatisticsCustomerMapper {
+
+    /**
+     * 新建客户数(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerSummaryByDateRespVO> selectCustomerCreateCountGroupByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 成交客户数(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerSummaryByDateRespVO> selectCustomerDealCountGroupByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 新建客户数(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerSummaryByUserRespVO> selectCustomerCreateCountGroupByUser(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 成交客户数(按用户)
+     *
+     * @param reqVO 请求参数@param reqVO 请求参数@param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerSummaryByUserRespVO> selectCustomerDealCountGroupByUser(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 合同总金额(按用户)
+     * @return 统计数据@return 统计数据@param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerSummaryByUserRespVO> selectContractPriceGroupByUser(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 合同回款金额(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerSummaryByUserRespVO> selectReceivablePriceGroupByUser(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 跟进次数(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsFollowUpSummaryByDateRespVO> selectFollowUpRecordCountGroupByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 跟进客户数(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsFollowUpSummaryByDateRespVO> selectFollowUpCustomerCountGroupByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 跟进次数(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsFollowUpSummaryByUserRespVO> selectFollowUpRecordCountGroupByUser(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 跟进客户数(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsFollowUpSummaryByUserRespVO> selectFollowUpCustomerCountGroupByUser(CrmStatisticsCustomerReqVO reqVO);
+
+
+    /**
+     * 首次合同、回款信息(用于【客户转化率】页面)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerContractSummaryRespVO> selectContractSummary(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 跟进次数(按类型)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsFollowUpSummaryByTypeRespVO> selectFollowUpRecordCountGroupByType(CrmStatisticsCustomerReqVO reqVO);
+
+
+    /**
+     * 进入公海客户数(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    // TODO: @芋艿 模拟数据, 需要增加 crm_owner_record 表
+    default List<CrmStatisticsPoolSummaryByDateRespVO> selectPoolCustomerPutCountByDate(CrmStatisticsCustomerReqVO reqVO) {
+        LocalDateTime currrentDate = LocalDateTimeUtil.beginOfDay(reqVO.getTimes()[0]);
+        LocalDateTime endDate = LocalDateTimeUtil.endOfDay(reqVO.getTimes()[1]);
+        List<CrmStatisticsPoolSummaryByDateRespVO> voList = new ArrayList<>();
+        while (currrentDate.isBefore(endDate)) {
+            voList.add(new CrmStatisticsPoolSummaryByDateRespVO()
+                .setTime(LocalDateTimeUtil.format(currrentDate, "yyyy-MM-dd"))
+                .setCustomerPutCount(RandomUtil.randomInt(0, 10))
+                .setCustomerTakeCount(RandomUtil.randomInt(0, 10)));
+            currrentDate = currrentDate.plusDays(1);
+        }
+
+        return voList;
+    }
+
+    /**
+     * 公海领取客户数(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    // TODO: @芋艿 模拟数据, 需要增加 crm_owner_record 表
+    default List<CrmStatisticsPoolSummaryByDateRespVO> selectPoolCustomerTakeCountByDate(CrmStatisticsCustomerReqVO reqVO) {
+        return selectPoolCustomerPutCountByDate(reqVO);
+    }
+
+    /**
+     * 进入公海客户数(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    // TODO: @芋艿 模拟数据, 需要增加 crm_owner_record 表
+    default List<CrmStatisticsPoolSummaryByUserRespVO> selectPoolCustomerPutCountByUser(CrmStatisticsCustomerReqVO reqVO) {
+        return convertList(reqVO.getUserIds(), userId ->
+            (CrmStatisticsPoolSummaryByUserRespVO) new CrmStatisticsPoolSummaryByUserRespVO()
+                .setCustomerPutCount(RandomUtil.randomInt(0, 10))
+                .setCustomerTakeCount(RandomUtil.randomInt(0, 10))
+                .setOwnerUserId(userId));
+    }
+
+    /**
+     * 公海领取客户数(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    // TODO: @芋艿 模拟数据, 需要增加 crm_owner_record 表
+    default List<CrmStatisticsPoolSummaryByUserRespVO> selectPoolCustomerTakeCountByUser(CrmStatisticsCustomerReqVO reqVO) {
+        return selectPoolCustomerPutCountByUser(reqVO);
+    }
+
+    /**
+     * 客户成交周期(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerDealCycleByDateRespVO> selectCustomerDealCycleGroupByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 客户成交周期(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerDealCycleByUserRespVO> selectCustomerDealCycleGroupByUser(CrmStatisticsCustomerReqVO reqVO);
+
+}

+ 41 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsPerformanceMapper.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
+
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * CRM 员工业绩分析 Mapper
+ *
+ * @author scholar
+ */
+@Mapper
+public interface CrmStatisticsPerformanceMapper {
+
+    /**
+     * 员工签约合同数量
+     *
+     * @param performanceReqVO 参数
+     * @return 员工签约合同数量
+     */
+    List<CrmStatisticsPerformanceRespVO> selectContractCountPerformance(CrmStatisticsPerformanceReqVO performanceReqVO);
+
+    /**
+     * 员工签约合同金额
+     *
+     * @param performanceReqVO 参数
+     * @return 员工签约合同金额
+     */
+    List<CrmStatisticsPerformanceRespVO> selectContractPricePerformance(CrmStatisticsPerformanceReqVO performanceReqVO);
+
+    /**
+     * 员工回款金额
+     *
+     * @param performanceReqVO 参数
+     * @return 员工回款金额
+     */
+    List<CrmStatisticsPerformanceRespVO> selectReceivablePricePerformance(CrmStatisticsPerformanceReqVO performanceReqVO);
+
+}

+ 24 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsPortraitMapper.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
+
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.*;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * CRM 数据画像 Mapper
+ *
+ * @author HUIHUI
+ */
+@Mapper
+public interface CrmStatisticsPortraitMapper {
+
+    List<CrmStatisticCustomerAreaRespVO> selectSummaryListGroupByAreaId(CrmStatisticsPortraitReqVO reqVO);
+
+    List<CrmStatisticCustomerIndustryRespVO> selectCustomerIndustryListGroupByIndustryId(CrmStatisticsPortraitReqVO reqVO);
+
+    List<CrmStatisticCustomerSourceRespVO> selectCustomerSourceListGroupBySource(CrmStatisticsPortraitReqVO reqVO);
+
+    List<CrmStatisticCustomerLevelRespVO> selectCustomerLevelListGroupByLevel(CrmStatisticsPortraitReqVO reqVO);
+
+}

+ 11 - 11
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsRankingMapper.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsRankMapper.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
 
-import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRankReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankReqVO;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.List;
@@ -12,7 +12,7 @@ import java.util.List;
  * @author anhaohao
  */
 @Mapper
-public interface CrmStatisticsRankingMapper {
+public interface CrmStatisticsRankMapper {
 
     /**
      * 查询合同金额排行榜
@@ -20,7 +20,7 @@ public interface CrmStatisticsRankingMapper {
      * @param rankReqVO 参数
      * @return 合同金额排行榜
      */
-    List<CrmStatisticsRanKRespVO> selectContractPriceRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> selectContractPriceRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 查询回款金额排行榜
@@ -28,7 +28,7 @@ public interface CrmStatisticsRankingMapper {
      * @param rankReqVO 参数
      * @return 回款金额排行榜
      */
-    List<CrmStatisticsRanKRespVO> selectReceivablePriceRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> selectReceivablePriceRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 查询签约合同数量排行榜
@@ -36,7 +36,7 @@ public interface CrmStatisticsRankingMapper {
      * @param rankReqVO 参数
      * @return 签约合同数量排行榜
      */
-    List<CrmStatisticsRanKRespVO> selectContractCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> selectContractCountRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 查询产品销量排行榜
@@ -44,7 +44,7 @@ public interface CrmStatisticsRankingMapper {
      * @param rankReqVO 参数
      * @return 产品销量排行榜
      */
-    List<CrmStatisticsRanKRespVO> selectProductSalesRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> selectProductSalesRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 查询新增客户数排行榜
@@ -52,7 +52,7 @@ public interface CrmStatisticsRankingMapper {
      * @param rankReqVO 参数
      * @return 新增客户数排行榜
      */
-    List<CrmStatisticsRanKRespVO> selectCustomerCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> selectCustomerCountRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 查询联系人数量排行榜
@@ -60,7 +60,7 @@ public interface CrmStatisticsRankingMapper {
      * @param rankReqVO 参数
      * @return 联系人数量排行榜
      */
-    List<CrmStatisticsRanKRespVO> selectContactsCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> selectContactsCountRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 查询跟进次数排行榜
@@ -68,7 +68,7 @@ public interface CrmStatisticsRankingMapper {
      * @param rankReqVO 参数
      * @return 跟进次数排行榜
      */
-    List<CrmStatisticsRanKRespVO> selectFollowCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> selectFollowCountRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 查询跟进客户数排行榜
@@ -76,6 +76,6 @@ public interface CrmStatisticsRankingMapper {
      * @param rankReqVO 参数
      * @return 跟进客户数排行榜
      */
-    List<CrmStatisticsRanKRespVO> selectFollowCustomerCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> selectFollowCustomerCountRank(CrmStatisticsRankReqVO rankReqVO);
 
 }

+ 33 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/excel/core/AreaExcelColumnSelectFunction.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.crm.framework.excel.core;
+
+import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction;
+import cn.iocoder.yudao.framework.ip.core.Area;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 地区下拉框数据源的 {@link ExcelColumnSelectFunction} 实现类
+ *
+ * @author HUIHUI
+ */
+@Service
+public class AreaExcelColumnSelectFunction implements ExcelColumnSelectFunction {
+
+    public static final String NAME = "getCrmAreaNameList"; // 防止和别的模块重名
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public List<String> getOptions() {
+        // 获取地区下拉数据
+        // TODO @puhui999:嘿嘿,这里改成省份、城市、区域,三个选项,难度大么?
+        Area area = AreaUtils.getArea(Area.ID_CHINA);
+        return AreaUtils.getAreaNodePathList(area.getChildren());
+    }
+
+}

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/excel/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * crm 模块的 excel 拓展封装
+ */
+package cn.iocoder.yudao.module.crm.framework.excel;

+ 3 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/package-info.java

@@ -1 +1,4 @@
+/**
+ * crm 模块的 operatelog 拓展封装
+ */
 package cn.iocoder.yudao.module.crm.framework.operatelog;

+ 3 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/package-info.java

@@ -1 +1,4 @@
+/**
+ * crm 模块的 permission 拓展封装
+ */
 package cn.iocoder.yudao.module.crm.framework.permission;

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/web/package-info.java

@@ -1,4 +1,4 @@
 /**
- * trade 模块的 web 配置
+ * crm 模块的 web 拓展封装
  */
 package cn.iocoder.yudao.module.crm.framework.web;

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

@@ -46,8 +46,8 @@ public interface CrmBusinessService {
     /**
      * 更新商机相关跟进信息
      *
-     * @param id 编号
-     * @param contactNextTime 下次联系时间
+     * @param id                 编号
+     * @param contactNextTime    下次联系时间
      * @param contactLastContent 最后联系内容
      */
     void updateBusinessFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
@@ -55,7 +55,7 @@ public interface CrmBusinessService {
     /**
      * 更新商机的下次联系时间
      *
-     * @param ids 编号数组
+     * @param ids             编号数组
      * @param contactNextTime 下次联系时间
      */
     void updateBusinessContactNextTime(Collection<Long> ids, LocalDateTime contactNextTime);
@@ -185,4 +185,13 @@ public interface CrmBusinessService {
         return status.getName();
     }
 
+    /**
+     * 获得商机列表
+     *
+     * @param customerId  客户编号
+     * @param ownerUserId 负责人编号
+     * @return 商机列表
+     */
+    List<CrmBusinessDO> getBusinessListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId);
+
 }

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

@@ -88,7 +88,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
             success = CRM_BUSINESS_CREATE_SUCCESS)
     public Long createBusiness(CrmBusinessSaveReqVO createReqVO, Long userId) {
         // 1.1 校验产品项的有效性
-        List<CrmBusinessProductDO> businessProducts = validateBusinessProducts(createReqVO.getProducts());
+        List<CrmBusinessProductDO> businessProducts = validateBusinessProducts(createReqVO.getBusinessProducts());
         // 1.2 校验关联字段
         validateRelationDataExists(createReqVO);
 
@@ -129,7 +129,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         // 1.1 校验存在
         CrmBusinessDO oldBusiness = validateBusinessExists(updateReqVO.getId());
         // 1.2 校验产品项的有效性
-        List<CrmBusinessProductDO> businessProducts = validateBusinessProducts(updateReqVO.getProducts());
+        List<CrmBusinessProductDO> businessProducts = validateBusinessProducts(updateReqVO.getBusinessProducts());
         // 1.3 校验关联字段
         validateRelationDataExists(updateReqVO);
 
@@ -202,9 +202,9 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         }
     }
 
-    private List<CrmBusinessProductDO> validateBusinessProducts(List<CrmBusinessSaveReqVO.Product> list) {
+    private List<CrmBusinessProductDO> validateBusinessProducts(List<CrmBusinessSaveReqVO.BusinessProduct> list) {
         // 1. 校验产品存在
-         productService.validProductList(convertSet(list, CrmBusinessSaveReqVO.Product::getProductId));
+        productService.validProductList(convertSet(list, CrmBusinessSaveReqVO.BusinessProduct::getProductId));
         // 2. 转化为 CrmBusinessProductDO 列表
         return convertList(list, o -> BeanUtils.toBean(o, CrmBusinessProductDO.class,
                 item -> item.setTotalPrice(MoneyUtils.priceMultiply(item.getBusinessPrice(), item.getCount()))));
@@ -234,7 +234,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         }
         // 1.4 校验是不是状态没变更
         if ((reqVO.getStatusId() != null && reqVO.getStatusId().equals(business.getStatusId()))
-            || (reqVO.getEndStatus() != null && reqVO.getEndStatus().equals(business.getEndStatus()))) {
+                || (reqVO.getEndStatus() != null && reqVO.getEndStatus().equals(business.getEndStatus()))) {
             throw exception(BUSINESS_UPDATE_STATUS_FAIL_STATUS_EQUALS);
         }
 
@@ -301,7 +301,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
         // 2.1 数据权限转移
         permissionService.transferPermission(new CrmPermissionTransferReqBO(userId, CrmBizTypeEnum.CRM_BUSINESS.getType(),
-                reqVO.getNewOwnerUserId(), reqVO.getId(), CrmPermissionLevelEnum.OWNER.getLevel()));
+                reqVO.getId(), reqVO.getNewOwnerUserId(), reqVO.getOldOwnerPermissionLevel()));
         // 2.2 设置新的负责人
         businessMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
 
@@ -370,4 +370,9 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         return businessMapper.selectCountByStatusTypeId(statusTypeId);
     }
 
+    @Override
+    public List<CrmBusinessDO> getBusinessListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
+        return businessMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
+    }
+
 }

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

@@ -75,8 +75,8 @@ public interface CrmContactService {
     /**
      * 更新联系人的下次联系时间
      *
-     * @param ids                编号数组
-     * @param contactNextTime    下次联系时间
+     * @param ids             编号数组
+     * @param contactNextTime 下次联系时间
      */
     void updateContactContactNextTime(Collection<Long> ids, LocalDateTime contactNextTime);
 
@@ -160,4 +160,13 @@ public interface CrmContactService {
      */
     Long getContactCountByCustomerId(Long customerId);
 
+    /**
+     * 获得联系人列表
+     *
+     * @param customerId  客户编号
+     * @param ownerUserId 负责人编号
+     * @return 联系人列表
+     */
+    List<CrmContactDO> getContactListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId);
+
 }

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

@@ -298,4 +298,9 @@ public class CrmContactServiceImpl implements CrmContactService {
         return contactMapper.selectCount(CrmContactDO::getCustomerId, customerId);
     }
 
+    @Override
+    public List<CrmContactDO> getContactListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
+        return contactMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
+    }
+
 }

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

@@ -58,8 +58,8 @@ public interface CrmContractService {
     /**
      * 更新合同相关的更进信息
      *
-     * @param id               合同编号
-     * @param contactNextTime  下次联系时间
+     * @param id                 合同编号
+     * @param contactNextTime    下次联系时间
      * @param contactLastContent 最后联系内容
      */
     void updateContractFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
@@ -75,7 +75,7 @@ public interface CrmContractService {
     /**
      * 更新合同流程审批结果
      *
-     * @param id 合同编号
+     * @param id        合同编号
      * @param bpmResult BPM 审批结果
      */
     void updateContractAuditStatus(Long id, Integer bpmResult);
@@ -193,4 +193,13 @@ public interface CrmContractService {
      */
     Long getRemindContractCount(Long userId);
 
+    /**
+     * 获得合同列表
+     *
+     * @param customerId  客户编号
+     * @param ownerUserId 负责人编号
+     * @return 合同列表
+     */
+    List<CrmContractDO> getContractListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId);
+
 }

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

@@ -30,12 +30,14 @@ import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import cn.iocoder.yudao.module.crm.service.product.CrmProductService;
+import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.service.impl.DiffParseFunction;
 import com.mzt.logapi.starter.annotation.LogRecord;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -86,7 +88,9 @@ public class CrmContractServiceImpl implements CrmContractService {
     private CrmContactService contactService;
     @Resource
     private CrmContractConfigService contractConfigService;
-
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private CrmReceivableService receivableService;
     @Resource
     private AdminUserApi adminUserApi;
     @Resource
@@ -222,15 +226,19 @@ public class CrmContractServiceImpl implements CrmContractService {
             success = CRM_CONTRACT_DELETE_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
     public void deleteContract(Long id) {
-        // TODO @puhui999:如果被 CrmReceivableDO 所使用,则不允许删除
-        // 校验存在
+        // 1.1 校验存在
         CrmContractDO contract = validateContractExists(id);
-        // 删除
+        // 1.2 如果被 CrmReceivableDO 所使用,则不允许删除
+        if (receivableService.getReceivableCountByContractId(contract.getId()) > 0) {
+            throw exception(CONTRACT_DELETE_FAIL);
+        }
+
+        // 2.1 删除合同
         contractMapper.deleteById(id);
-        // 删除数据权限
+        // 2.2 删除数据权限
         crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_CONTRACT.getType(), id);
 
-        // 记录操作日志上下文
+        // 3. 记录操作日志上下文
         LogRecordContext.putVariable("contractName", contract.getName());
     }
 
@@ -399,4 +407,9 @@ public class CrmContractServiceImpl implements CrmContractService {
         return contractMapper.selectCountByRemind(userId, config);
     }
 
+    @Override
+    public List<CrmContractDO> getContractListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
+        return contractMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
+    }
+
 }

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

@@ -9,7 +9,13 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactTransferReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractTransferReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
@@ -201,7 +207,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         CrmCustomerDO customer = validateCustomerExists(reqVO.getId());
         // 1.2 校验拥有客户是否到达上限
         validateCustomerExceedOwnerLimit(reqVO.getNewOwnerUserId(), 1);
-
         // 2.1 数据权限转移
         permissionService.transferPermission(new CrmPermissionTransferReqBO(userId, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
                 reqVO.getId(), reqVO.getNewOwnerUserId(), reqVO.getOldOwnerPermissionLevel()));
@@ -209,10 +214,45 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         customerMapper.updateById(new CrmCustomerDO().setId(reqVO.getId())
                 .setOwnerUserId(reqVO.getNewOwnerUserId()).setOwnerTime(LocalDateTime.now()));
 
+        // 2.3 同时转移
+        if (CollUtil.isNotEmpty(reqVO.getToBizTypes())) {
+            transfer(reqVO, userId);
+        }
+
         // 3. 记录转移日志
         LogRecordContext.putVariable("customer", customer);
     }
 
+    /**
+     * 转移客户时,需要额外有【联系人】【商机】【合同】
+     *
+     * @param reqVO  请求
+     * @param userId 用户编号
+     */
+    private void transfer(CrmCustomerTransferReqVO reqVO, Long userId) {
+        if (reqVO.getToBizTypes().contains(CrmBizTypeEnum.CRM_CONTACT.getType())) {
+            List<CrmContactDO> contactList = contactService.getContactListByCustomerIdOwnerUserId(reqVO.getId(), userId);
+            contactList.forEach(item -> {
+                contactService.transferContact(new CrmContactTransferReqVO(item.getId(), reqVO.getNewOwnerUserId(),
+                        reqVO.getOldOwnerPermissionLevel()), userId);
+            });
+        }
+        if (reqVO.getToBizTypes().contains(CrmBizTypeEnum.CRM_BUSINESS.getType())) {
+            List<CrmBusinessDO> businessList = businessService.getBusinessListByCustomerIdOwnerUserId(reqVO.getId(), userId);
+            businessList.forEach(item -> {
+                businessService.transferBusiness(new CrmBusinessTransferReqVO(item.getId(), reqVO.getNewOwnerUserId(),
+                        reqVO.getOldOwnerPermissionLevel()), userId);
+            });
+        }
+        if (reqVO.getToBizTypes().contains(CrmBizTypeEnum.CRM_CONTRACT.getType())) {
+            List<CrmContractDO> contractList = contractService.getContractListByCustomerIdOwnerUserId(reqVO.getId(), userId);
+            contractList.forEach(item -> {
+                contractService.transferContract(new CrmContractTransferReqVO(item.getId(), reqVO.getNewOwnerUserId(),
+                        reqVO.getOldOwnerPermissionLevel()), userId);
+            });
+        }
+    }
+
     @Override
     @LogRecord(type = CRM_CUSTOMER_TYPE, subType = CRM_CUSTOMER_LOCK_SUB_TYPE, bizNo = "{{#lockReqVO.id}}",
             success = CRM_CUSTOMER_LOCK_SUCCESS)

+ 13 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.permission;
 
 
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionSaveReqVO;
 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.enums.common.CrmBizTypeEnum;
@@ -19,6 +20,14 @@ import java.util.List;
  */
 public interface CrmPermissionService {
 
+    /**
+     * 创建数据权限
+     *
+     * @param reqVO  创建信息
+     * @param userId 用户编号
+     */
+    void createPermission(CrmPermissionSaveReqVO reqVO, Long userId);
+
     /**
      * 创建数据权限
      *
@@ -111,10 +120,10 @@ public interface CrmPermissionService {
     /**
      * 校验是否有指定数据的操作权限
      *
-     * @param bizType   数据类型,关联 {@link CrmBizTypeEnum}
-     * @param bizId     数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
-     * @param userId    用户编号
-     * @param level 权限级别
+     * @param bizType 数据类型,关联 {@link CrmBizTypeEnum}
+     * @param bizId   数据编号,关联 {@link CrmBizTypeEnum} 对应模块 DO#getId()
+     * @param userId  用户编号
+     * @param level   权限级别
      * @return 是否有权限
      */
     boolean hasPermission(Integer bizType, Long bizId, Long userId, CrmPermissionLevelEnum level);

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

@@ -4,28 +4,34 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionSaveReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
+import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
+import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum.isOwner;
 
@@ -40,18 +46,119 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
 
     @Resource
     private CrmPermissionMapper permissionMapper;
-
+    @Resource
+    @Lazy // 解决依赖循环
+    private CrmContactService contactService;
+    @Resource
+    @Lazy // 解决依赖循环
+    private CrmBusinessService businessService;
+    @Resource
+    @Lazy // 解决依赖循环
+    private CrmContractService contractService;
     @Resource
     private AdminUserApi adminUserApi;
 
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @CrmPermission(bizTypeValue = "#reqVO.bizType", bizId = "#reqVO.bizId", level = CrmPermissionLevelEnum.OWNER)
+    public void createPermission(CrmPermissionSaveReqVO reqVO, Long userId) {
+        // 1. 创建数据权限
+        createPermission0(BeanUtils.toBean(reqVO, CrmPermissionCreateReqBO.class));
+
+        // 2. 处理【同时添加至】的权限
+        if (CollUtil.isEmpty(reqVO.getToBizTypes())) {
+            return;
+        }
+        List<CrmPermissionCreateReqBO> createPermissions = new ArrayList<>();
+        buildContactPermissions(reqVO, userId, createPermissions);
+        buildBusinessPermissions(reqVO, userId, createPermissions);
+        buildContractPermissions(reqVO, userId, createPermissions);
+        if (CollUtil.isEmpty(createPermissions)) {
+            return;
+        }
+        createPermissionBatch(createPermissions);
+    }
+
+    /**
+     * 处理同时添加至联系人
+     *
+     * @param reqVO             请求
+     * @param userId            操作人
+     * @param createPermissions 待添加权限列表
+     */
+    private void buildContactPermissions(CrmPermissionSaveReqVO reqVO, Long userId, List<CrmPermissionCreateReqBO> createPermissions) {
+        // 1. 校验是否被同时添加
+        Integer type = CrmBizTypeEnum.CRM_CONTACT.getType();
+        if (!reqVO.getToBizTypes().contains(type)) {
+            return;
+        }
+        // 2. 添加数据权限
+        List<CrmContactDO> contactList = contactService.getContactListByCustomerIdOwnerUserId(reqVO.getBizId(), userId);
+        contactList.forEach(item -> createBizTypePermissions(reqVO, type, item.getId(), item.getName(), createPermissions));
+    }
+
+    /**
+     * 处理同时添加至商机
+     *
+     * @param reqVO             请求
+     * @param userId            操作人
+     * @param createPermissions 待添加权限列表
+     */
+    private void buildBusinessPermissions(CrmPermissionSaveReqVO reqVO, Long userId, List<CrmPermissionCreateReqBO> createPermissions) {
+        // 1. 校验是否被同时添加
+        Integer type = CrmBizTypeEnum.CRM_BUSINESS.getType();
+        if (!reqVO.getToBizTypes().contains(type)) {
+            return;
+        }
+        // 2. 添加数据权限
+        List<CrmBusinessDO> businessList = businessService.getBusinessListByCustomerIdOwnerUserId(reqVO.getBizId(), userId);
+        businessList.forEach(item -> createBizTypePermissions(reqVO, type, item.getId(), item.getName(), createPermissions));
+    }
+
+    /**
+     * 处理同时添加至合同
+     *
+     * @param reqVO             请求
+     * @param userId            操作人
+     * @param createPermissions 待添加权限列表
+     */
+    private void buildContractPermissions(CrmPermissionSaveReqVO reqVO, Long userId, List<CrmPermissionCreateReqBO> createPermissions) {
+        // 1. 校验是否被同时添加
+        Integer type = CrmBizTypeEnum.CRM_CONTRACT.getType();
+        if (!reqVO.getToBizTypes().contains(type)) {
+            return;
+        }
+        // 2. 添加数据权限
+        List<CrmContractDO> contractList = contractService.getContractListByCustomerIdOwnerUserId(reqVO.getBizId(), userId);
+        contractList.forEach(item -> createBizTypePermissions(reqVO, type, item.getId(), item.getName(), createPermissions));
+    }
+
+    private void createBizTypePermissions(CrmPermissionSaveReqVO reqVO, Integer type, Long bizId, String name,
+                                          List<CrmPermissionCreateReqBO> createPermissions) {
+        AdminUserRespDTO user = adminUserApi.getUser(reqVO.getUserId());
+        // 1. 需要考虑,被添加人,是不是应该有对应的权限了;
+        CrmPermissionDO permission = hasAnyPermission(type, bizId, reqVO.getUserId());
+        if (ObjUtil.isNotNull(permission)) {
+            throw exception(CRM_PERMISSION_CREATE_FAIL_EXISTS, user.getNickname(), CrmBizTypeEnum.getNameByType(type),
+                    name, CrmPermissionLevelEnum.getNameByLevel(permission.getLevel()));
+        }
+        // 2. 添加数据权限
+        createPermissions.add(new CrmPermissionCreateReqBO().setBizType(type)
+                .setBizId(bizId).setUserId(reqVO.getUserId()).setLevel(reqVO.getLevel()));
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createPermission(CrmPermissionCreateReqBO createReqBO) {
+        return createPermission0(createReqBO);
+    }
+
+    private Long createPermission0(CrmPermissionCreateReqBO createReqBO) {
         validatePermissionNotExists(Collections.singletonList(createReqBO));
         // 1. 校验用户是否存在
         adminUserApi.validateUserList(Collections.singletonList(createReqBO.getUserId()));
-
-        // 2. 创建
+        // 2. 插入权限
         CrmPermissionDO permission = BeanUtils.toBean(createReqBO, CrmPermissionDO.class);
         permissionMapper.insert(permission);
         return permission.getId();
@@ -170,7 +277,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
             throw exception(CRM_PERMISSION_DELETE_FAIL);
         }
         // 校验操作人是否为负责人
-        CrmPermissionDO permission = permissionMapper.selectByBizIdAndUserId(permissions.get(0).getBizId(), userId);
+        CrmPermissionDO permission = permissionMapper.selectByBizAndUserId(permissions.get(0).getBizType(), permissions.get(0).getBizId(), userId);
         if (permission == null) {
             throw exception(CRM_PERMISSION_DELETE_DENIED);
         }
@@ -220,4 +327,9 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
                 ObjUtil.equal(permission.getUserId(), userId) && ObjUtil.equal(permission.getLevel(), level.getLevel()));
     }
 
+    public CrmPermissionDO hasAnyPermission(Integer bizType, Long bizId, Long userId) {
+        List<CrmPermissionDO> permissionList = permissionMapper.selectByBizTypeAndBizId(bizType, bizId);
+        return findFirst(permissionList, permission -> ObjUtil.equal(permission.getUserId(), userId));
+    }
+
 }

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

@@ -20,7 +20,6 @@ import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.service.impl.DiffParseFunction;
 import com.mzt.logapi.starter.annotation.LogRecord;
 import jakarta.annotation.Resource;
-import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -46,9 +45,6 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
     @Resource
     private CrmReceivablePlanMapper receivablePlanMapper;
 
-    @Resource
-    @Lazy // 延迟加载,避免循环依赖
-    private CrmReceivableService receivableService;
     @Resource
     private CrmContractService contractService;
     @Resource
@@ -144,7 +140,7 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
         // 2. 删除
         receivablePlanMapper.deleteById(id);
         // 3. 删除数据权限
-        permissionService.deletePermission(CrmBizTypeEnum.CRM_CUSTOMER.getType(), id);
+        permissionService.deletePermission(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(), id);
 
         // 4. 记录操作日志上下文
         LogRecordContext.putVariable("receivablePlan", receivablePlan);

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

@@ -122,4 +122,12 @@ public interface CrmReceivableService {
      */
     Map<Long, BigDecimal> getReceivablePriceMapByContractId(Collection<Long> contractIds);
 
+    /**
+     * 根据合同编号查询回款数量
+     *
+     * @param contractId 合同编号
+     * @return 回款数量
+     */
+    Long getReceivableCountByContractId(Long contractId);
+
 }

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

@@ -37,10 +37,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import java.math.BigDecimal;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
@@ -81,7 +78,6 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     @Resource
     private BpmProcessInstanceApi bpmProcessInstanceApi;
 
-    // TODO @puhui999:操作日志没记录上
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_CREATE_SUB_TYPE, bizNo = "{{#receivable.id}}",
@@ -115,6 +111,7 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
 
         // 5. 记录操作日志上下文
         LogRecordContext.putVariable("receivable", receivable);
+        LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId()));
         return receivable.getId();
     }
 
@@ -156,7 +153,6 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
         }
     }
 
-    // TODO @puhui999:操作日志没记录上
     @Override
     @Transactional(rollbackFor = Exception.class)
     @LogRecord(type = CRM_RECEIVABLE_TYPE, subType = CRM_RECEIVABLE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
@@ -164,11 +160,14 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_RECEIVABLE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
     public void updateReceivable(CrmReceivableSaveReqVO updateReqVO) {
         Assert.notNull(updateReqVO.getId(), "回款编号不能为空");
-        // 1.1 校验可回款金额超过上限
-        validateReceivablePriceExceedsLimit(updateReqVO);
         updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段
-        // 1.2 校验存在
+        // 1.1 校验存在
         CrmReceivableDO receivable = validateReceivableExists(updateReqVO.getId());
+        updateReqVO.setOwnerUserId(receivable.getOwnerUserId()).setCustomerId(receivable.getCustomerId())
+                .setContractId(receivable.getContractId()).setPlanId(receivable.getPlanId()); // 设置已存在的值
+        // 1.2 校验可回款金额超过上限
+        validateReceivablePriceExceedsLimit(updateReqVO);
+
         // 1.3 只有草稿、审批中,可以编辑;
         if (!ObjectUtils.equalsAny(receivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                 CrmAuditStatusEnum.PROCESS.getStatus())) {
@@ -180,8 +179,17 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
         receivableMapper.updateById(updateObj);
 
         // 3. 记录操作日志上下文
-        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(receivable, CrmReceivableSaveReqVO.class));
         LogRecordContext.putVariable("receivable", receivable);
+        LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId()));
+        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(receivable, CrmReceivableSaveReqVO.class));
+    }
+
+    private Integer getReceivablePeriod(Long planId) {
+        if (Objects.isNull(planId)) {
+            return null;
+        }
+        CrmReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(planId);
+        return receivablePlan.getPeriod();
     }
 
     @Override
@@ -212,15 +220,19 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
         if (receivable.getPlanId() != null && receivablePlanService.getReceivablePlan(receivable.getPlanId()) != null) {
             throw exception(RECEIVABLE_DELETE_FAIL);
         }
-        // TODO @puhui999:审批通过时,不允许删除;
+        // 1.3 审批通过时,不允许删除
+        if (ObjUtil.equal(receivable.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
+            throw exception(RECEIVABLE_DELETE_FAIL_IS_APPROVE);
+        }
 
-        // 2. 删除
+        // 2.1 删除回款
         receivableMapper.deleteById(id);
-        // 3. 删除数据权限
+        // 2.2 删除数据权限
         permissionService.deletePermission(CrmBizTypeEnum.CRM_RECEIVABLE.getType(), id);
 
-        // 4. 记录操作日志上下文
+        // 3. 记录操作日志上下文
         LogRecordContext.putVariable("receivable", receivable);
+        LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId()));
     }
 
     @Override
@@ -289,4 +301,9 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
         return receivableMapper.selectReceivablePriceMapByContractId(contractIds);
     }
 
+    @Override
+    public Long getReceivableCountByContractId(Long contractId) {
+        return receivableMapper.selectCountByContractId(contractId);
+    }
+
 }

+ 96 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerService.java

@@ -0,0 +1,96 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
+
+import java.util.List;
+
+/**
+ * CRM 客户分析 Service 接口
+ *
+ * @author dhb52
+ */
+public interface CrmStatisticsCustomerService {
+
+    /**
+     * 总量分析(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerSummaryByDateRespVO> getCustomerSummaryByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 总量分析(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerSummaryByUserRespVO> getCustomerSummaryByUser(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 跟进次数分析(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsFollowUpSummaryByDateRespVO> getFollowUpSummaryByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 跟进次数分析(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsFollowUpSummaryByUserRespVO> getFollowUpSummaryByUser(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 客户跟进次数分析(按类型)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsFollowUpSummaryByTypeRespVO> getFollowUpSummaryByType(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 获取客户的首次合同、回款信息列表,用于【客户转化率】页面
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerContractSummaryRespVO> getContractSummary(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 公海客户分析(按日期)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsPoolSummaryByDateRespVO> getPoolSummaryByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 公海客户分析(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsPoolSummaryByUserRespVO> getPoolSummaryByUser(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 客户成交周期(按日期)
+     *
+     * 成交周期的定义:客户 customer 在创建出来,到合同 contract 第一次成交的时间差
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerDealCycleByDateRespVO> getCustomerDealCycleByDate(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 客户成交周期(按用户)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerDealCycleByUserRespVO> getCustomerDealCycleByUser(CrmStatisticsCustomerReqVO reqVO);
+
+}

+ 323 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java

@@ -0,0 +1,323 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
+import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsCustomerMapper;
+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 jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
+
+/**
+ * CRM 客户分析 Service 实现类
+ *
+ * @author dhb52
+ */
+@Service
+@Validated
+public class CrmStatisticsCustomerServiceImpl implements CrmStatisticsCustomerService {
+
+    @Resource
+    private CrmStatisticsCustomerMapper customerMapper;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
+
+    @Override
+    public List<CrmStatisticsCustomerSummaryByDateRespVO> getCustomerSummaryByDate(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按天统计,获取分项统计数据
+        List<CrmStatisticsCustomerSummaryByDateRespVO> customerCreateCountList = customerMapper.selectCustomerCreateCountGroupByDate(reqVO);
+        List<CrmStatisticsCustomerSummaryByDateRespVO> customerDealCountList = customerMapper.selectCustomerDealCountGroupByDate(reqVO);
+
+        // 3. 按照日期间隔,合并数据
+        List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+        return convertList(timeRanges, times -> {
+            Integer customerCreateCount = customerCreateCountList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToInt(CrmStatisticsCustomerSummaryByDateRespVO::getCustomerCreateCount).sum();
+            Integer customerDealCount = customerDealCountList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToInt(CrmStatisticsCustomerSummaryByDateRespVO::getCustomerDealCount).sum();
+            return new CrmStatisticsCustomerSummaryByDateRespVO()
+                    .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+                    .setCustomerCreateCount(customerCreateCount).setCustomerDealCount(customerDealCount);
+        });
+    }
+
+    @Override
+    public List<CrmStatisticsCustomerSummaryByUserRespVO> getCustomerSummaryByUser(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按用户统计,获取分项统计数据
+        List<CrmStatisticsCustomerSummaryByUserRespVO> customerCreateCountList = customerMapper.selectCustomerCreateCountGroupByUser(reqVO);
+        List<CrmStatisticsCustomerSummaryByUserRespVO> customerDealCountList = customerMapper.selectCustomerDealCountGroupByUser(reqVO);
+        List<CrmStatisticsCustomerSummaryByUserRespVO> contractPriceList = customerMapper.selectContractPriceGroupByUser(reqVO);
+        List<CrmStatisticsCustomerSummaryByUserRespVO> receivablePriceList = customerMapper.selectReceivablePriceGroupByUser(reqVO);
+
+        // 3.1 按照用户,合并统计数据
+        List<CrmStatisticsCustomerSummaryByUserRespVO> summaryList = convertList(reqVO.getUserIds(), userId -> {
+            Integer customerCreateCount = customerCreateCountList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                    .mapToInt(CrmStatisticsCustomerSummaryByUserRespVO::getCustomerCreateCount).sum();
+            Integer customerDealCount = customerDealCountList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                    .mapToInt(CrmStatisticsCustomerSummaryByUserRespVO::getCustomerDealCount).sum();
+            BigDecimal contractPrice = contractPriceList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                    .reduce(BigDecimal.ZERO, (sum, vo) -> sum.add(vo.getContractPrice()), BigDecimal::add);
+            BigDecimal receivablePrice = receivablePriceList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                    .reduce(BigDecimal.ZERO, (sum, vo) -> sum.add(vo.getReceivablePrice()), BigDecimal::add);
+            return (CrmStatisticsCustomerSummaryByUserRespVO) new CrmStatisticsCustomerSummaryByUserRespVO()
+                    .setCustomerCreateCount(customerCreateCount).setCustomerDealCount(customerDealCount)
+                    .setContractPrice(contractPrice).setReceivablePrice(receivablePrice).setOwnerUserId(userId);
+        });
+        // 3.2 拼接用户信息
+        appendUserInfo(summaryList);
+        return summaryList;
+    }
+
+    @Override
+    public List<CrmStatisticsFollowUpSummaryByDateRespVO> getFollowUpSummaryByDate(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按天统计,获取分项统计数据
+        List<CrmStatisticsFollowUpSummaryByDateRespVO> followUpRecordCountList = customerMapper.selectFollowUpRecordCountGroupByDate(reqVO);
+        List<CrmStatisticsFollowUpSummaryByDateRespVO> followUpCustomerCountList = customerMapper.selectFollowUpCustomerCountGroupByDate(reqVO);
+
+        // 3. 按照时间间隔,合并统计数据
+        List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+        return convertList(timeRanges, times -> {
+            Integer followUpRecordCount = followUpRecordCountList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToInt(CrmStatisticsFollowUpSummaryByDateRespVO::getFollowUpRecordCount).sum();
+            Integer followUpCustomerCount = followUpCustomerCountList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToInt(CrmStatisticsFollowUpSummaryByDateRespVO::getFollowUpCustomerCount).sum();
+            return new CrmStatisticsFollowUpSummaryByDateRespVO()
+                    .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+                    .setFollowUpCustomerCount(followUpRecordCount).setFollowUpRecordCount(followUpCustomerCount);
+        });
+    }
+
+    @Override
+    public List<CrmStatisticsFollowUpSummaryByUserRespVO> getFollowUpSummaryByUser(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按用户统计,获取分项统计数据
+        List<CrmStatisticsFollowUpSummaryByUserRespVO> followUpRecordCountList = customerMapper.selectFollowUpRecordCountGroupByUser(reqVO);
+        List<CrmStatisticsFollowUpSummaryByUserRespVO> followUpCustomerCountList = customerMapper.selectFollowUpCustomerCountGroupByUser(reqVO);
+
+        // 3.1 按照用户,合并统计数据
+        List<CrmStatisticsFollowUpSummaryByUserRespVO> summaryList = convertList(reqVO.getUserIds(), userId -> {
+            Integer followUpRecordCount = followUpRecordCountList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                    .mapToInt(CrmStatisticsFollowUpSummaryByUserRespVO::getFollowUpRecordCount).sum();
+            Integer followUpCustomerCount = followUpCustomerCountList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                    .mapToInt(CrmStatisticsFollowUpSummaryByUserRespVO::getFollowUpCustomerCount).sum();
+            return (CrmStatisticsFollowUpSummaryByUserRespVO) new CrmStatisticsFollowUpSummaryByUserRespVO()
+                    .setFollowUpCustomerCount(followUpRecordCount).setFollowUpRecordCount(followUpCustomerCount).setOwnerUserId(userId);
+        });
+        // 3.2 拼接用户信息
+        appendUserInfo(summaryList);
+        return summaryList;
+    }
+
+    @Override
+    public List<CrmStatisticsFollowUpSummaryByTypeRespVO> getFollowUpSummaryByType(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 获得跟进数据
+        return customerMapper.selectFollowUpRecordCountGroupByType(reqVO);
+    }
+
+    @Override
+    public List<CrmStatisticsCustomerContractSummaryRespVO> getContractSummary(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按用户统计,获取统计数据
+        List<CrmStatisticsCustomerContractSummaryRespVO> summaryList = customerMapper.selectContractSummary(reqVO);
+
+        // 3. 拼接信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSetByFlatMap(summaryList, vo -> Stream.of(NumberUtils.parseLong(vo.getCreator()), vo.getOwnerUserId())));
+        summaryList.forEach(vo -> {
+            findAndThen(userMap, NumberUtils.parseLong(vo.getCreator()), user -> vo.setCreatorUserName(user.getNickname()));
+            findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerUserName(user.getNickname()));
+        });
+        return summaryList;
+    }
+
+    @Override
+    public List<CrmStatisticsPoolSummaryByDateRespVO> getPoolSummaryByDate(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按天统计,获取分项统计数据
+        List<CrmStatisticsPoolSummaryByDateRespVO> customerPutCountList = customerMapper.selectPoolCustomerPutCountByDate(reqVO);
+        List<CrmStatisticsPoolSummaryByDateRespVO> customerTakeCountList = customerMapper.selectPoolCustomerTakeCountByDate(reqVO);
+
+        // 3. 按照日期间隔,合并数据
+        List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+        return convertList(timeRanges, times -> {
+            Integer customerPutCount = customerPutCountList.stream()
+                .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                .mapToInt(CrmStatisticsPoolSummaryByDateRespVO::getCustomerPutCount).sum();
+            Integer customerTakeCount = customerTakeCountList.stream()
+                .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                .mapToInt(CrmStatisticsPoolSummaryByDateRespVO::getCustomerTakeCount).sum();
+            return new CrmStatisticsPoolSummaryByDateRespVO()
+                .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+                .setCustomerPutCount(customerPutCount).setCustomerTakeCount(customerTakeCount);
+        });
+    }
+
+    @Override
+    public List<CrmStatisticsPoolSummaryByUserRespVO> getPoolSummaryByUser(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按用户统计,获取分项统计数据
+        List<CrmStatisticsPoolSummaryByUserRespVO> customerPutCountList = customerMapper.selectPoolCustomerPutCountByUser(reqVO);
+        List<CrmStatisticsPoolSummaryByUserRespVO> customerTakeCountList = customerMapper.selectPoolCustomerTakeCountByUser(reqVO);
+
+        // 3.1 按照用户,合并统计数据
+        List<CrmStatisticsPoolSummaryByUserRespVO> summaryList = convertList(reqVO.getUserIds(), userId -> {
+            Integer customerPutCount = customerPutCountList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                .mapToInt(CrmStatisticsPoolSummaryByUserRespVO::getCustomerPutCount).sum();
+            Integer customerTakeCount = customerTakeCountList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                .mapToInt(CrmStatisticsPoolSummaryByUserRespVO::getCustomerTakeCount).sum();
+            return (CrmStatisticsPoolSummaryByUserRespVO) new CrmStatisticsPoolSummaryByUserRespVO()
+                .setCustomerPutCount(customerPutCount).setCustomerTakeCount(customerTakeCount)
+                .setOwnerUserId(userId);
+        });
+        // 3.2 拼接用户信息
+        appendUserInfo(summaryList);
+        return summaryList;
+    }
+
+    @Override
+    public List<CrmStatisticsCustomerDealCycleByDateRespVO> getCustomerDealCycleByDate(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按天统计,获取分项统计数据
+        List<CrmStatisticsCustomerDealCycleByDateRespVO> customerDealCycleList = customerMapper.selectCustomerDealCycleGroupByDate(reqVO);
+
+        // 3. 按照日期间隔,合并统计数据
+        List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+        return convertList(timeRanges, times -> {
+            Double customerDealCycle = customerDealCycleList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToDouble(CrmStatisticsCustomerDealCycleByDateRespVO::getCustomerDealCycle).sum();
+            return new CrmStatisticsCustomerDealCycleByDateRespVO()
+                    .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+                    .setCustomerDealCycle(customerDealCycle);
+        });
+    }
+
+    @Override
+    public List<CrmStatisticsCustomerDealCycleByUserRespVO> getCustomerDealCycleByUser(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按用户统计,获取分项统计数据
+        List<CrmStatisticsCustomerDealCycleByUserRespVO> customerDealCycleList = customerMapper.selectCustomerDealCycleGroupByUser(reqVO);
+        List<CrmStatisticsCustomerSummaryByUserRespVO> customerDealCountList = customerMapper.selectCustomerDealCountGroupByUser(reqVO);
+
+        // 3.1 按照用户,合并统计数据
+        List<CrmStatisticsCustomerDealCycleByUserRespVO> summaryList = convertList(reqVO.getUserIds(), userId -> {
+            Double customerDealCycle = customerDealCycleList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                    .mapToDouble(CrmStatisticsCustomerDealCycleByUserRespVO::getCustomerDealCycle).sum();
+            Integer customerDealCount = customerDealCountList.stream().filter(vo -> userId.equals(vo.getOwnerUserId()))
+                    .mapToInt(CrmStatisticsCustomerSummaryByUserRespVO::getCustomerDealCount).sum();
+            return (CrmStatisticsCustomerDealCycleByUserRespVO) new CrmStatisticsCustomerDealCycleByUserRespVO()
+                    .setCustomerDealCycle(customerDealCycle).setCustomerDealCount(customerDealCount).setOwnerUserId(userId);
+        });
+        // 3.2 拼接用户信息
+        appendUserInfo(summaryList);
+        return summaryList;
+    }
+
+    /**
+     * 拼接用户信息(昵称)
+     *
+     * @param voList 统计数据
+     */
+    private <T extends CrmStatisticsCustomerByUserBaseRespVO> void appendUserInfo(List<T> voList) {
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(voList, CrmStatisticsCustomerByUserBaseRespVO::getOwnerUserId));
+        voList.forEach(vo -> findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerUserName(user.getNickname())));
+    }
+
+    /**
+     * 获取用户编号数组。如果用户编号为空, 则获得部门下的用户编号数组,包括子部门的所有用户编号
+     *
+     * @param reqVO 请求参数
+     * @return 用户编号数组
+     */
+    private List<Long> getUserIds(CrmStatisticsCustomerReqVO reqVO) {
+        // 情况一:选中某个用户
+        if (ObjUtil.isNotNull(reqVO.getUserId())) {
+            return List.of(reqVO.getUserId());
+        }
+        // 情况二:选中某个部门
+        // 2.1 获得部门列表
+        List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
+        deptIds.add(reqVO.getDeptId());
+        // 2.2 获得用户编号
+        return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
+    }
+
+}

+ 42 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceService.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+
+
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
+
+import java.util.List;
+
+/**
+ * CRM 员工绩效统计 Service 接口
+ *
+ * @author scholar
+ */
+public interface CrmStatisticsPerformanceService {
+
+    /**
+     * 员工签约合同数量分析
+     *
+     * @param performanceReqVO 排行参数
+     * @return 员工签约合同数量排行分析
+     */
+    List<CrmStatisticsPerformanceRespVO> getContractCountPerformance(CrmStatisticsPerformanceReqVO performanceReqVO);
+
+    /**
+     * 员工签约合同金额分析
+     *
+     * @param performanceReqVO 排行参数
+     * @return 员工签约合同金额分析
+     */
+    List<CrmStatisticsPerformanceRespVO> getContractPricePerformance(CrmStatisticsPerformanceReqVO performanceReqVO);
+
+    /**
+     * 员工获得回款金额分析
+     *
+     * @param performanceReqVO 排行参数
+     * @return 员工获得回款金额分析
+     */
+    List<CrmStatisticsPerformanceRespVO> getReceivablePricePerformance(CrmStatisticsPerformanceReqVO performanceReqVO);
+
+
+}

+ 102 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java

@@ -0,0 +1,102 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
+import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsPerformanceMapper;
+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 org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import jakarta.annotation.Resource;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * CRM 员工业绩分析 Service 实现类
+ *
+ * @author scholar
+ */
+@Service
+@Validated
+public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerformanceService {
+
+    @Resource
+    private CrmStatisticsPerformanceMapper performanceMapper;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
+
+    @Override
+    public List<CrmStatisticsPerformanceRespVO> getContractCountPerformance(CrmStatisticsPerformanceReqVO performanceReqVO) {
+        // TODO @scholar:我们可以换个思路实现,减少数据库的计算量;
+        // 比如说,2024 年的合同数据,是不是 2022-12 到 2024-12-31,每个月的统计呢?
+        // 理解之后,我们可以数据 group by 年-月,20222-12 到 2024-12-31 的,然后内存在聚合出 CrmStatisticsPerformanceRespVO 这样
+        // 这样,我们就可以减少数据库的计算量,提升性能;同时 SQL 也会很简单,开发者理解起来也简单哈;
+        return getPerformance(performanceReqVO, performanceMapper::selectContractCountPerformance);
+    }
+
+    @Override
+    public List<CrmStatisticsPerformanceRespVO> getContractPricePerformance(CrmStatisticsPerformanceReqVO performanceReqVO) {
+        return getPerformance(performanceReqVO, performanceMapper::selectContractPricePerformance);
+    }
+
+    @Override
+    public List<CrmStatisticsPerformanceRespVO> getReceivablePricePerformance(CrmStatisticsPerformanceReqVO performanceReqVO) {
+        return getPerformance(performanceReqVO, performanceMapper::selectReceivablePricePerformance);
+    }
+
+    /**
+     * 获得员工业绩数据
+     *
+     * @param performanceReqVO  参数
+     * @param performanceFunction 排行榜方法
+     * @return 排行版数据
+     */
+    private List<CrmStatisticsPerformanceRespVO> getPerformance(CrmStatisticsPerformanceReqVO performanceReqVO, Function<CrmStatisticsPerformanceReqVO,
+            List<CrmStatisticsPerformanceRespVO>> performanceFunction) {
+
+        // 1. 获得用户编号数组
+        final List<Long> userIds = getUserIds(performanceReqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        performanceReqVO.setUserIds(userIds);
+        // 2. 获得排行数据
+        List<CrmStatisticsPerformanceRespVO> performance = performanceFunction.apply(performanceReqVO);
+        if (CollUtil.isEmpty(performance)) {
+            return Collections.emptyList();
+        }
+        return performance;
+    }
+
+    /**
+     * 获取用户编号数组。如果用户编号为空, 则获得部门下的用户编号数组,包括子部门的所有用户编号
+     *
+     * @param reqVO 请求参数
+     * @return 用户编号数组
+     */
+    private List<Long> getUserIds(CrmStatisticsPerformanceReqVO reqVO) {
+        // 情况一:选中某个用户
+        if (ObjUtil.isNotNull(reqVO.getUserId())) {
+            return List.of(reqVO.getUserId());
+        }
+        // 情况二:选中某个部门
+        // 2.1 获得部门列表
+        final Long deptId = reqVO.getDeptId();
+        List<Long> deptIds = convertList(deptApi.getChildDeptList(deptId), DeptRespDTO::getId);
+        deptIds.add(deptId);
+        // 2.2 获得用户编号
+        return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
+    }
+
+}

+ 46 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitService.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.*;
+
+import java.util.List;
+
+/**
+ * CRM 客户画像 Service 接口
+ *
+ * @author HUIHUI
+ */
+public interface CrmStatisticsPortraitService {
+
+    /**
+     * 获取客户地区统计数据
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticCustomerAreaRespVO> getCustomerSummaryByArea(CrmStatisticsPortraitReqVO reqVO);
+
+    /**
+     * 获取客户行业统计数据
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticCustomerIndustryRespVO> getCustomerSummaryByIndustry(CrmStatisticsPortraitReqVO reqVO);
+
+    /**
+     * 获取客户级别统计数据
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticCustomerLevelRespVO> getCustomerSummaryByLevel(CrmStatisticsPortraitReqVO reqVO);
+
+    /**
+     * 获取客户来源统计数据
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticCustomerSourceRespVO> getCustomerSummaryBySource(CrmStatisticsPortraitReqVO reqVO);
+
+}

+ 128 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitServiceImpl.java

@@ -0,0 +1,128 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.ip.core.Area;
+import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.*;
+import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsPortraitMapper;
+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 jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
+
+/**
+ * CRM 客户画像 Service 实现类
+ *
+ * @author HUIHUI
+ */
+@Service
+public class CrmStatisticsPortraitServiceImpl implements CrmStatisticsPortraitService {
+
+    @Resource
+    private CrmStatisticsPortraitMapper portraitMapper;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
+
+    @Override
+    public List<CrmStatisticCustomerAreaRespVO> getCustomerSummaryByArea(CrmStatisticsPortraitReqVO reqVO) {
+        // 1. 获得用户编号数组
+        List<Long> userIds = getUserIds(reqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        reqVO.setUserIds(userIds);
+
+        // 2. 获取客户地区统计数据
+        List<CrmStatisticCustomerAreaRespVO> list = portraitMapper.selectSummaryListGroupByAreaId(reqVO);
+        if (CollUtil.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+
+        // 3. 拼接数据
+        List<Area> areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
+        areaList.add(new Area().setId(null).setName("未知")); // TODO @puhui999:是不是 65 find 的逻辑改下;不用 findAndThen,直接从 areaMap 拿;拿到就设置,不拿到就设置 null 和 未知;这样,58 本行可以删除掉完事了;这样代码更简单和一致
+        Map<Integer, Area> areaMap = convertMap(areaList, Area::getId);
+        return convertList(list, item -> {
+            Integer parentId = AreaUtils.getParentIdByType(item.getAreaId(), AreaTypeEnum.PROVINCE);
+            if (parentId == null) { // 找不到,归到未知
+                return item.setAreaId(null).setAreaName("未知");
+            }
+            findAndThen(areaMap, parentId, area -> item.setAreaId(parentId).setAreaName(area.getName()));
+            return item;
+        });
+    }
+
+    @Override
+    public List<CrmStatisticCustomerIndustryRespVO> getCustomerSummaryByIndustry(CrmStatisticsPortraitReqVO reqVO) {
+        // 1. 获得用户编号数组
+        List<Long> userIds = getUserIds(reqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        reqVO.setUserIds(userIds);
+
+        // 2. 获取客户行业统计数据
+        return portraitMapper.selectCustomerIndustryListGroupByIndustryId(reqVO);
+    }
+
+    @Override
+    public List<CrmStatisticCustomerSourceRespVO> getCustomerSummaryBySource(CrmStatisticsPortraitReqVO reqVO) {
+        // 1. 获得用户编号数组
+        List<Long> userIds = getUserIds(reqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        reqVO.setUserIds(userIds);
+
+        // 2. 获取客户行业统计数据
+        return portraitMapper.selectCustomerSourceListGroupBySource(reqVO);
+    }
+
+    @Override
+    public List<CrmStatisticCustomerLevelRespVO> getCustomerSummaryByLevel(CrmStatisticsPortraitReqVO reqVO) {
+        // 1. 获得用户编号数组
+        List<Long> userIds = getUserIds(reqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        reqVO.setUserIds(userIds);
+
+        // 2. 获取客户级别统计数据
+        return portraitMapper.selectCustomerLevelListGroupByLevel(reqVO);
+    }
+
+    /**
+     * 获取用户编号数组。如果用户编号为空, 则获得部门下的用户编号数组,包括子部门的所有用户编号
+     *
+     * @param reqVO 请求参数
+     * @return 用户编号数组
+     */
+    private List<Long> getUserIds(CrmStatisticsPortraitReqVO reqVO) {
+        // 情况一:选中某个用户
+        if (ObjUtil.isNotNull(reqVO.getUserId())) {
+            return List.of(reqVO.getUserId());
+        }
+        // 情况二:选中某个部门
+        // 2.1 获得部门列表
+        List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
+        deptIds.add(reqVO.getDeptId());
+        // 2.2 获得用户编号
+        return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
+    }
+
+}

+ 11 - 11
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsRankingService.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsRankService.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.crm.service.statistics;
 
 
-import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRankReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankReqVO;
 
 import java.util.List;
 
@@ -11,7 +11,7 @@ import java.util.List;
  *
  * @author anhaohao
  */
-public interface CrmStatisticsRankingService {
+public interface CrmStatisticsRankService {
 
     /**
      * 获得合同金额排行榜
@@ -19,7 +19,7 @@ public interface CrmStatisticsRankingService {
      * @param rankReqVO 排行参数
      * @return 合同金额排行榜
      */
-    List<CrmStatisticsRanKRespVO> getContractPriceRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> getContractPriceRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 获得回款金额排行榜
@@ -27,7 +27,7 @@ public interface CrmStatisticsRankingService {
      * @param rankReqVO 排行参数
      * @return 回款金额排行榜
      */
-    List<CrmStatisticsRanKRespVO> getReceivablePriceRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> getReceivablePriceRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 获得签约合同数量排行榜
@@ -35,7 +35,7 @@ public interface CrmStatisticsRankingService {
      * @param rankReqVO 排行参数
      * @return 签约合同数量排行榜
      */
-    List<CrmStatisticsRanKRespVO> getContractCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> getContractCountRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 获得产品销量排行榜
@@ -43,7 +43,7 @@ public interface CrmStatisticsRankingService {
      * @param rankReqVO 排行参数
      * @return 产品销量排行榜
      */
-    List<CrmStatisticsRanKRespVO> getProductSalesRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> getProductSalesRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 获得新增客户数排行榜
@@ -51,7 +51,7 @@ public interface CrmStatisticsRankingService {
      * @param rankReqVO 排行参数
      * @return 新增客户数排行榜
      */
-    List<CrmStatisticsRanKRespVO> getCustomerCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> getCustomerCountRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 获得联系人数量排行榜
@@ -59,7 +59,7 @@ public interface CrmStatisticsRankingService {
      * @param rankReqVO 排行参数
      * @return 联系人数量排行榜
      */
-    List<CrmStatisticsRanKRespVO> getContactsCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> getContactsCountRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 获得跟进次数排行榜
@@ -67,7 +67,7 @@ public interface CrmStatisticsRankingService {
      * @param rankReqVO 排行参数
      * @return 跟进次数排行榜
      */
-    List<CrmStatisticsRanKRespVO> getFollowCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> getFollowCountRank(CrmStatisticsRankReqVO rankReqVO);
 
     /**
      * 获得跟进客户数排行榜
@@ -75,6 +75,6 @@ public interface CrmStatisticsRankingService {
      * @param rankReqVO 排行参数
      * @return 跟进客户数排行榜
      */
-    List<CrmStatisticsRanKRespVO> getFollowCustomerCountRank(CrmStatisticsRankReqVO rankReqVO);
+    List<CrmStatisticsRankRespVO> getFollowCustomerCountRank(CrmStatisticsRankReqVO rankReqVO);
 
 }

+ 19 - 19
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsRankingServiceImpl.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsRankServiceImpl.java

@@ -2,9 +2,9 @@ package cn.iocoder.yudao.module.crm.service.statistics;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
-import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRankReqVO;
-import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsRankingMapper;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO;
+import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsRankMapper;
 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;
@@ -29,10 +29,10 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
  */
 @Service
 @Validated
-public class CrmStatisticsRankingServiceImpl implements CrmStatisticsRankingService {
+public class CrmStatisticsRankServiceImpl implements CrmStatisticsRankService {
 
     @Resource
-    private CrmStatisticsRankingMapper rankMapper;
+    private CrmStatisticsRankMapper rankMapper;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -40,64 +40,64 @@ public class CrmStatisticsRankingServiceImpl implements CrmStatisticsRankingServ
     private DeptApi deptApi;
 
     @Override
-    public List<CrmStatisticsRanKRespVO> getContractPriceRank(CrmStatisticsRankReqVO rankReqVO) {
+    public List<CrmStatisticsRankRespVO> getContractPriceRank(CrmStatisticsRankReqVO rankReqVO) {
         return getRank(rankReqVO, rankMapper::selectContractPriceRank);
     }
 
     @Override
-    public List<CrmStatisticsRanKRespVO> getReceivablePriceRank(CrmStatisticsRankReqVO rankReqVO) {
+    public List<CrmStatisticsRankRespVO> getReceivablePriceRank(CrmStatisticsRankReqVO rankReqVO) {
         return getRank(rankReqVO, rankMapper::selectReceivablePriceRank);
     }
 
     @Override
-    public List<CrmStatisticsRanKRespVO> getContractCountRank(CrmStatisticsRankReqVO rankReqVO) {
+    public List<CrmStatisticsRankRespVO> getContractCountRank(CrmStatisticsRankReqVO rankReqVO) {
         return getRank(rankReqVO, rankMapper::selectContractCountRank);
     }
 
     @Override
-    public List<CrmStatisticsRanKRespVO> getProductSalesRank(CrmStatisticsRankReqVO rankReqVO) {
+    public List<CrmStatisticsRankRespVO> getProductSalesRank(CrmStatisticsRankReqVO rankReqVO) {
         return getRank(rankReqVO, rankMapper::selectProductSalesRank);
     }
 
     @Override
-    public List<CrmStatisticsRanKRespVO> getCustomerCountRank(CrmStatisticsRankReqVO rankReqVO) {
+    public List<CrmStatisticsRankRespVO> getCustomerCountRank(CrmStatisticsRankReqVO rankReqVO) {
         return getRank(rankReqVO, rankMapper::selectCustomerCountRank);
     }
 
     @Override
-    public List<CrmStatisticsRanKRespVO> getContactsCountRank(CrmStatisticsRankReqVO rankReqVO) {
+    public List<CrmStatisticsRankRespVO> getContactsCountRank(CrmStatisticsRankReqVO rankReqVO) {
         return getRank(rankReqVO, rankMapper::selectContactsCountRank);
     }
 
     @Override
-    public List<CrmStatisticsRanKRespVO> getFollowCountRank(CrmStatisticsRankReqVO rankReqVO) {
+    public List<CrmStatisticsRankRespVO> getFollowCountRank(CrmStatisticsRankReqVO rankReqVO) {
         return getRank(rankReqVO, rankMapper::selectFollowCountRank);
     }
 
     @Override
-    public List<CrmStatisticsRanKRespVO> getFollowCustomerCountRank(CrmStatisticsRankReqVO rankReqVO) {
+    public List<CrmStatisticsRankRespVO> getFollowCustomerCountRank(CrmStatisticsRankReqVO rankReqVO) {
         return getRank(rankReqVO, rankMapper::selectFollowCustomerCountRank);
     }
 
     /**
      * 获得排行版数据
      *
-     * @param rankReqVO  参数
+     * @param rankReqVO    参数
      * @param rankFunction 排行榜方法
      * @return 排行版数据
      */
-    private List<CrmStatisticsRanKRespVO> getRank(CrmStatisticsRankReqVO rankReqVO, Function<CrmStatisticsRankReqVO, List<CrmStatisticsRanKRespVO>> rankFunction) {
+    private List<CrmStatisticsRankRespVO> getRank(CrmStatisticsRankReqVO rankReqVO, Function<CrmStatisticsRankReqVO, List<CrmStatisticsRankRespVO>> rankFunction) {
         // 1. 获得用户编号数组
         rankReqVO.setUserIds(getUserIds(rankReqVO.getDeptId()));
         if (CollUtil.isEmpty(rankReqVO.getUserIds())) {
             return Collections.emptyList();
         }
         // 2. 获得排行数据
-        List<CrmStatisticsRanKRespVO> ranks = rankFunction.apply(rankReqVO);
+        List<CrmStatisticsRankRespVO> ranks = rankFunction.apply(rankReqVO);
         if (CollUtil.isEmpty(ranks)) {
             return Collections.emptyList();
         }
-        ranks.sort(Comparator.comparing(CrmStatisticsRanKRespVO::getCount).reversed());
+        ranks.sort(Comparator.comparing(CrmStatisticsRankRespVO::getCount).reversed());
         // 3. 拼接用户信息
         appendUserInfo(ranks);
         return ranks;
@@ -108,8 +108,8 @@ public class CrmStatisticsRankingServiceImpl implements CrmStatisticsRankingServ
      *
      * @param ranks 排行榜数据
      */
-    private void appendUserInfo(List<CrmStatisticsRanKRespVO> ranks) {
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(ranks, CrmStatisticsRanKRespVO::getOwnerUserId));
+    private void appendUserInfo(List<CrmStatisticsRankRespVO> ranks) {
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(ranks, CrmStatisticsRankRespVO::getOwnerUserId));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
         ranks.forEach(rank -> MapUtils.findAndThen(userMap, rank.getOwnerUserId(), user -> {
             rank.setNickname(user.getNickname());

+ 224 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml

@@ -0,0 +1,224 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsCustomerMapper">
+
+    <select id="selectCustomerCreateCountGroupByDate"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO">
+        SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
+               COUNT(*) AS customerCreateCount
+          FROM crm_customer
+         WHERE deleted = 0
+           AND owner_user_id IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+         GROUP BY time
+    </select>
+
+    <!-- TODO 芋艿:应该不用过滤时间 -->
+    <select id="selectCustomerDealCountGroupByDate"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO">
+        SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time,
+               COUNT(DISTINCT customer.id) AS customer_deal_count
+          FROM crm_customer AS customer
+                LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
+         WHERE customer.deleted = 0 AND contract.deleted = 0
+           AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+           AND customer.owner_user_id IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+         GROUP BY time
+    </select>
+
+    <select id="selectCustomerCreateCountGroupByUser"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO">
+        SELECT owner_user_id,
+               COUNT(*) AS customer_create_count
+          FROM crm_customer
+         WHERE deleted = 0
+           AND owner_user_id in
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+         GROUP BY owner_user_id
+    </select>
+
+    <select id="selectCustomerDealCountGroupByUser"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO">
+        SELECT customer.owner_user_id,
+               COUNT(DISTINCT customer.id) AS customer_deal_count
+          FROM crm_customer AS customer
+                LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
+         WHERE customer.deleted = 0 AND contract.deleted = 0
+           AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+           AND customer.owner_user_id IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+         GROUP BY customer.owner_user_id
+    </select>
+
+    <select id="selectContractPriceGroupByUser"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO">
+        SELECT owner_user_id,
+               IFNULL(SUM(total_price), 0) AS contract_price
+          FROM crm_contract
+         WHERE deleted = 0
+           AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+           AND owner_user_id in
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND order_date BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+         GROUP BY owner_user_id
+    </select>
+
+    <select id="selectReceivablePriceGroupByUser"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO">
+        SELECT owner_user_id,
+               IFNULL(SUM(price), 0) AS receivable_price
+          FROM crm_receivable
+         WHERE deleted = 0
+           AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+           AND owner_user_id IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND return_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+         GROUP BY owner_user_id
+    </select>
+
+    <select id="selectFollowUpRecordCountGroupByDate"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByDateRespVO">
+        SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
+               COUNT(*) AS follow_up_record_count
+          FROM crm_follow_up_record
+         WHERE creator IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+           AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
+         GROUP BY time
+    </select>
+
+    <select id="selectFollowUpCustomerCountGroupByDate"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByDateRespVO">
+        SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
+               COUNT(DISTINCT biz_id) AS follow_up_customer_count
+          FROM crm_follow_up_record
+         WHERE creator IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+           AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
+         GROUP BY time
+    </select>
+
+    <select id="selectFollowUpRecordCountGroupByUser"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByUserRespVO">
+        SELECT creator as owner_user_id,
+               COUNT(*) AS follow_up_record_count
+          FROM crm_follow_up_record
+         WHERE creator IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+           AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
+         GROUP BY creator
+    </select>
+
+    <select id="selectFollowUpCustomerCountGroupByUser"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByUserRespVO">
+        SELECT
+            creator as owner_user_id,
+            COUNT(DISTINCT biz_id) AS follow_up_customer_count
+        FROM crm_follow_up_record
+        WHERE creator IN
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+        AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
+        GROUP BY creator
+    </select>
+
+    <select id="selectFollowUpRecordCountGroupByType"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByTypeRespVO">
+        SELECT type AS follow_up_type,
+               COUNT(*) AS follow_up_record_count
+          FROM crm_follow_up_record
+         WHERE creator IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+           AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
+         GROUP BY follow_up_type
+    </select>
+
+    <select id="selectContractSummary"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerContractSummaryRespVO">
+        SELECT customer.name AS customer_name,
+               customer.industry_id,
+               customer.source,
+               customer.owner_user_id,
+               customer.creator,
+               customer.create_time,
+               contract.name AS contract_name,
+               contract.total_price,
+               contract.order_date,
+               IFNULL(receivable.price, 0) AS receivable_price
+          FROM crm_customer AS customer
+                INNER JOIN crm_contract AS contract ON customer.id = contract.customer_id
+                LEFT JOIN crm_receivable AS receivable ON contract.id = receivable.contract_id
+         WHERE customer.deleted = 0 AND contract.deleted = 0 AND receivable.deleted = 0
+           AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+           AND customer.owner_user_id IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+    </select>
+
+    <!-- TIMESTAMPDIFF 用于求差值;AVG 求平均;TRUNCATE 去掉小数点、只保留整数 -->
+    <select id="selectCustomerDealCycleGroupByDate"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByDateRespVO">
+        SELECT DATE_FORMAT(contract.order_date, '%Y-%m-%d') AS time,
+               IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle
+          FROM crm_customer AS customer
+                LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
+         WHERE customer.deleted = 0 AND contract.deleted = 0
+           AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+           AND customer.owner_user_id IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+         GROUP BY time
+    </select>
+
+    <select id="selectCustomerDealCycleGroupByUser"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByUserRespVO">
+        SELECT customer.owner_user_id,
+               IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle
+          FROM crm_customer AS customer
+                LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
+         WHERE customer.deleted = 0 AND contract.deleted = 0
+           AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+           AND customer.owner_user_id IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+           AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+         GROUP BY customer.owner_user_id
+    </select>
+
+</mapper>

+ 152 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml

@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsPerformanceMapper">
+
+    <select id="selectContractCountPerformance"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
+        SELECT
+        t.time as time,
+        COALESCE(t.currentMonthCount,0) as currentMonthCount,
+        COALESCE(y.lastMonthCount,0) as lastMonthCount,
+        COALESCE(z.lastYearCount,0) as lastYearCount
+        FROM
+        (SELECT
+        COUNT(1) AS currentMonthCount,
+        DATE_FORMAT(order_date, '%Y-%m') AS time
+        FROM	crm_contract
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
+        GROUP BY time)t
+        LEFT JOIN
+        (SELECT
+        COUNT(1) AS lastMonthCount,
+        DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 MONTH), '%Y-%m') AS time
+        FROM	crm_contract
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
+        or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
+        GROUP BY time)y ON t.time = y.time
+        LEFT JOIN
+        (SELECT
+        COUNT(1) AS lastYearCount,
+        DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 YEAR), '%Y-%m') AS time
+        FROM	crm_contract
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
+        GROUP BY time)z ON t.time = z.time
+    </select>
+    <select id="selectContractPricePerformance"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
+        SELECT
+        t.time as time,
+        COALESCE(t.currentMonthCount,0) as currentMonthCount,
+        COALESCE(y.lastMonthCount,0) as lastMonthCount,
+        COALESCE(z.lastYearCount,0) as lastYearCount
+        FROM
+        (SELECT
+        IFNULL(SUM(total_price), 0) AS currentMonthCount,
+        DATE_FORMAT(order_date, '%Y-%m') AS time
+        FROM	crm_contract
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
+        GROUP BY time)t
+        LEFT JOIN
+        (SELECT
+        IFNULL(SUM(total_price), 0) AS lastMonthCount,
+        DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 MONTH), '%Y-%m') AS time
+        FROM	crm_contract
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
+        or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
+        GROUP BY time)y ON t.time = y.time
+        LEFT JOIN
+        (SELECT
+        IFNULL(SUM(total_price), 0) AS lastYearCount,
+        DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 YEAR), '%Y-%m') AS time
+        FROM	crm_contract
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
+        GROUP BY time)z ON t.time = z.time
+    </select>
+
+    <select id="selectReceivablePricePerformance"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
+        SELECT
+        t.time as time,
+        COALESCE(t.currentMonthCount,0) as currentMonthCount,
+        COALESCE(y.lastMonthCount,0) as lastMonthCount,
+        COALESCE(z.lastYearCount,0) as lastYearCount
+        FROM
+        (SELECT
+        IFNULL(SUM(price), 0) AS currentMonthCount,
+        DATE_FORMAT(return_time, '%Y-%m') AS time
+        FROM	crm_receivable
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
+        GROUP BY time)t
+        LEFT JOIN
+        (SELECT
+        IFNULL(SUM(price), 0) AS lastMonthCount,
+        DATE_FORMAT(DATE_ADD(return_time,INTERVAL 1 MONTH), '%Y-%m') AS time
+        FROM	crm_receivable
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND (DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
+        or DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
+        GROUP BY time)y ON t.time = y.time
+        LEFT JOIN
+        (SELECT
+        IFNULL(SUM(price), 0) AS lastYearCount,
+        DATE_FORMAT(DATE_ADD(return_time,INTERVAL 1 YEAR), '%Y-%m') AS time
+        FROM	crm_receivable
+        WHERE deleted = 0
+        AND audit_status = 20
+        AND owner_user_id in
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
+        GROUP BY time)z ON t.time = z.time
+    </select>
+
+
+</mapper>

+ 61 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPortraitMapper.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsPortraitMapper">
+
+    <select id="selectSummaryListGroupByAreaId"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticCustomerAreaRespVO">
+        SELECT area_id, COUNT(*) AS customerCount, SUM(deal_status) AS dealCount
+        FROM crm_customer
+        WHERE deleted = 0 AND area_id IS NOT NULL
+        AND owner_user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime}
+        AND #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY area_id
+    </select>
+
+    <select id="selectCustomerIndustryListGroupByIndustryId"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticCustomerIndustryRespVO">
+        SELECT industry_id, COUNT(*) AS customerCount, SUM(deal_status) AS dealCount
+        FROM crm_customer
+        WHERE deleted = 0 AND industry_id IS NOT NULL
+        AND owner_user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime}
+        AND #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY industry_id
+    </select>
+
+    <select id="selectCustomerSourceListGroupBySource"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticCustomerSourceRespVO">
+        SELECT source, COUNT(*) AS customerCount, SUM(deal_status) AS dealCount
+        FROM crm_customer
+        WHERE deleted = 0 AND source IS NOT NULL
+        AND owner_user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime}
+        AND #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY source
+    </select>
+
+    <select id="selectCustomerLevelListGroupByLevel"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait.CrmStatisticCustomerLevelRespVO">
+        SELECT level, COUNT(*) AS customerCount, SUM(deal_status) AS dealCount
+        FROM crm_customer
+        WHERE deleted = 0 AND level IS NOT NULL
+        AND owner_user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime}
+        AND #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY level
+    </select>
+
+</mapper>

+ 51 - 59
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/bi/CrmBiRankingMapper.xml → yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsRankMapper.xml

@@ -1,125 +1,117 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsRankingMapper">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsRankMapper">
 
     <select id="selectContractPriceRank"
-            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO">
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
         SELECT IFNULL(SUM(total_price), 0) AS count, owner_user_id
         FROM crm_contract
         WHERE deleted = 0
-        AND audit_status = 20
+        AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
         and owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND order_date between #{times[0],javaType=java.time.LocalDateTime} and
-            #{times[1],javaType=java.time.LocalDateTime}
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND order_date between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime}
         GROUP BY owner_user_id
     </select>
 
     <select id="selectReceivablePriceRank"
-            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO">
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
         SELECT IFNULL(SUM(price), 0) AS count, owner_user_id
         FROM crm_receivable
         WHERE deleted = 0
-        AND audit_status = 20
+        AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
         AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND return_time between #{times[0],javaType=java.time.LocalDateTime} and
-            #{times[1],javaType=java.time.LocalDateTime}
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND return_time between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime}
         GROUP BY owner_user_id
     </select>
 
     <select id="selectContractCountRank"
-            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO">
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
         SELECT COUNT(1) AS count, owner_user_id
         FROM crm_contract
         WHERE deleted = 0
-        AND audit_status = 20
+        AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
         AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND order_date between #{times[0],javaType=java.time.LocalDateTime} and
-            #{times[1],javaType=java.time.LocalDateTime}
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND order_date between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime}
         GROUP BY owner_user_id
     </select>
 
-    <!-- TODO 待定 这里是否需要关联 crm_contract_product 表,计算销售额 -->
     <select id="selectProductSalesRank"
-            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO">
-        SELECT COUNT(1) AS count, owner_user_id
-        FROM crm_contract
-        WHERE deleted = 0
-        AND audit_status = 20
-        AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND order_date between #{times[0],javaType=java.time.LocalDateTime} and
-            #{times[1],javaType=java.time.LocalDateTime}
-        GROUP BY owner_user_id
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
+        SELECT COUNT(product.count) AS count, contract.owner_user_id
+        FROM crm_contract_product product
+        INNER JOIN crm_contract contract ON product.contract_id = contract.id
+        WHERE contract.deleted = 0 AND contract.deleted = 0
+        AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+        AND contract.owner_user_id in
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND contract.order_date between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY contract.owner_user_id
     </select>
 
     <select id="selectCustomerCountRank"
-            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO">
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
         SELECT COUNT(1) AS count, owner_user_id
         FROM crm_customer
         WHERE deleted = 0
         AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND create_time between #{times[0],javaType=java.time.LocalDateTime} and
-            #{times[1],javaType=java.time.LocalDateTime}
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND create_time between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime}
         GROUP BY owner_user_id
     </select>
 
     <select id="selectContactsCountRank"
-            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO">
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
         SELECT COUNT(1) AS count, owner_user_id
         FROM crm_contact
         WHERE deleted = 0
         AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND create_time between #{times[0],javaType=java.time.LocalDateTime} and
-            #{times[1],javaType=java.time.LocalDateTime}
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND create_time between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime}
         GROUP BY owner_user_id
     </select>
 
     <select id="selectFollowCountRank"
-            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO">
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
         SELECT COUNT(1) AS count, cc.owner_user_id
         FROM crm_follow_up_record AS cfur
         LEFT JOIN crm_contact AS cc ON FIND_IN_SET(cc.id, cfur.contact_ids)
         WHERE cfur.deleted = 0
         AND cc.deleted = 0
         AND cc.owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND cfur.create_time between #{times[0],javaType=java.time.LocalDateTime} and
-            #{times[1],javaType=java.time.LocalDateTime}
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND cfur.create_time between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime}
         GROUP BY cc.owner_user_id
     </select>
 
     <select id="selectFollowCustomerCountRank"
-            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.CrmStatisticsRanKRespVO">
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
         SELECT COUNT(DISTINCT cc.id) AS count, cc.owner_user_id
         FROM crm_follow_up_record AS cfur
         LEFT JOIN crm_contact AS cc ON FIND_IN_SET(cc.id, cfur.contact_ids)
         WHERE cfur.deleted = 0
         AND cc.deleted = 0
         AND cc.owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND cfur.create_time between #{times[0],javaType=java.time.LocalDateTime} and
-            #{times[1],javaType=java.time.LocalDateTime}
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        AND cfur.create_time between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime}
         GROUP BY cc.owner_user_id
     </select>
 

+ 7 - 7
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java

@@ -61,14 +61,14 @@ public class ErpPurchaseOrderController {
 
     @PostMapping("/create")
     @Operation(summary = "创建采购订单")
-    @PreAuthorize("@ss.hasPermission('erp:purchase-create:create')")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-order:create')")
     public CommonResult<Long> createPurchaseOrder(@Valid @RequestBody ErpPurchaseOrderSaveReqVO createReqVO) {
         return success(purchaseOrderService.createPurchaseOrder(createReqVO));
     }
 
     @PutMapping("/update")
     @Operation(summary = "更新采购订单")
-    @PreAuthorize("@ss.hasPermission('erp:purchase-create:update')")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-order:update')")
     public CommonResult<Boolean> updatePurchaseOrder(@Valid @RequestBody ErpPurchaseOrderSaveReqVO updateReqVO) {
         purchaseOrderService.updatePurchaseOrder(updateReqVO);
         return success(true);
@@ -76,7 +76,7 @@ public class ErpPurchaseOrderController {
 
     @PutMapping("/update-status")
     @Operation(summary = "更新采购订单的状态")
-    @PreAuthorize("@ss.hasPermission('erp:purchase-create:update-status')")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-order:update-status')")
     public CommonResult<Boolean> updatePurchaseOrderStatus(@RequestParam("id") Long id,
                                                       @RequestParam("status") Integer status) {
         purchaseOrderService.updatePurchaseOrderStatus(id, status);
@@ -86,7 +86,7 @@ public class ErpPurchaseOrderController {
     @DeleteMapping("/delete")
     @Operation(summary = "删除采购订单")
     @Parameter(name = "ids", description = "编号数组", required = true)
-    @PreAuthorize("@ss.hasPermission('erp:purchase-create:delete')")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-order:delete')")
     public CommonResult<Boolean> deletePurchaseOrder(@RequestParam("ids") List<Long> ids) {
         purchaseOrderService.deletePurchaseOrder(ids);
         return success(true);
@@ -95,7 +95,7 @@ public class ErpPurchaseOrderController {
     @GetMapping("/get")
     @Operation(summary = "获得采购订单")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
-    @PreAuthorize("@ss.hasPermission('erp:purchase-create:query')")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-order:query')")
     public CommonResult<ErpPurchaseOrderRespVO> getPurchaseOrder(@RequestParam("id") Long id) {
         ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.getPurchaseOrder(id);
         if (purchaseOrder == null) {
@@ -115,7 +115,7 @@ public class ErpPurchaseOrderController {
 
     @GetMapping("/page")
     @Operation(summary = "获得采购订单分页")
-    @PreAuthorize("@ss.hasPermission('erp:purchase-create:query')")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-order:query')")
     public CommonResult<PageResult<ErpPurchaseOrderRespVO>> getPurchaseOrderPage(@Valid ErpPurchaseOrderPageReqVO pageReqVO) {
         PageResult<ErpPurchaseOrderDO> pageResult = purchaseOrderService.getPurchaseOrderPage(pageReqVO);
         return success(buildPurchaseOrderVOPageResult(pageResult));
@@ -123,7 +123,7 @@ public class ErpPurchaseOrderController {
 
     @GetMapping("/export-excel")
     @Operation(summary = "导出采购订单 Excel")
-    @PreAuthorize("@ss.hasPermission('erp:purchase-create:export')")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-order:export')")
     @ApiAccessLog(operateType = EXPORT)
     public void exportPurchaseOrderExcel(@Valid ErpPurchaseOrderPageReqVO pageReqVO,
                                     HttpServletResponse response) throws IOException {

+ 7 - 4
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm

@@ -94,7 +94,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 const props = defineProps<{
-  ${subJoinColumn.javaField}: undefined // ${subJoinColumn.columnComment}(主表的关联字段)
+  ${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段)
 }>()
 const loading = ref(false) // 列表的加载中
 const list = ref([]) // 列表的数据
@@ -103,17 +103,20 @@ const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  ${subJoinColumn.javaField}: undefined
+  ${subJoinColumn.javaField}: undefined as unknown
 })
 
 /** 监听主表的关联字段的变化,加载对应的子表数据 */
 watch(
   () => props.${subJoinColumn.javaField},
-  (val) => {
+  (val: number) => {
+    if (!val) {
+      return
+    }
     queryParams.${subJoinColumn.javaField} = val
     handleQuery()
   },
-  { immediate: false }
+    { immediate: true, deep: true }
 )
 #end
 

+ 1 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java

@@ -101,7 +101,7 @@ public class AppProductSpuController {
             throw exception(SPU_NOT_EXISTS);
         }
         if (!ProductSpuStatusEnum.isEnable(spu.getStatus())) {
-            throw exception(SPU_NOT_ENABLE);
+            throw exception(SPU_NOT_ENABLE, spu.getName());
         }
         // 获得商品 SKU
         List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spu.getId());

+ 0 - 3
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStorePageReqVO.java

@@ -4,14 +4,11 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.validation.InEnum;
 import lombok.*;
 
-import java.time.LocalTime;
-import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import org.springframework.format.annotation.DateTimeFormat;
 import java.time.LocalDateTime;
 
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_HOUR_MINUTE_SECOND;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
 @Schema(description = "管理后台 - 自提门店分页 Request VO")

Някои файлове не бяха показани, защото твърде много файлове са промени