Browse Source

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

YunaiV 1 year ago
parent
commit
088fbb2096
100 changed files with 2303 additions and 1744 deletions
  1. 2 2
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  2. 36 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java
  3. 59 7
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java
  4. 2 2
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java
  5. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/test/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtilsTest.java
  6. 5 0
      yudao-framework/yudao-spring-boot-starter-excel/pom.xml
  7. 46 0
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java
  8. 27 0
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/enums/ExcelColumn.java
  9. 115 0
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java
  10. 28 8
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
  11. 0 27
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/listener/BpmResultListenerApi.java
  12. 0 32
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/listener/dto/BpmResultListenerRespDTO.java
  13. 2 4
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/event/BpmProcessInstanceResultEvent.java
  14. 1 1
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/event/BpmProcessInstanceResultEventListener.java
  15. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
  16. 1 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventPublisher.java
  17. 0 36
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/listener/BpmServiceResultListener.java
  18. 2 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveResultListener.java
  19. 1 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
  20. 27 21
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
  21. 37 13
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java
  22. 0 55
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/business/CrmBizEndStatus.java
  23. 45 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/business/CrmBusinessEndStatusEnum.java
  24. 1 1
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java
  25. 4 2
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionLevelEnum.java
  26. 0 27
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionRoleCodeEnum.java
  27. 43 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/receivable/CrmReceivableReturnTypeEnum.java
  28. 0 41
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/backlog/CrmBacklogController.java
  29. 0 37
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/backlog/vo/CrmTodayCustomerPageReqVO.java
  30. 0 9
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/bi/CrmBiRankController.http
  31. 0 32
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http
  32. 83 38
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
  33. 126 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessStatusController.java
  34. 0 141
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessStatusTypeController.java
  35. 0 75
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessExcelVO.java
  36. 104 29
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java
  37. 28 36
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessSaveReqVO.java
  38. 30 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessUpdateStatusReqVO.java
  39. 0 15
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/status/CrmBusinessStatusPageReqVO.java
  40. 0 19
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/status/CrmBusinessStatusQueryVO.java
  41. 35 17
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/status/CrmBusinessStatusRespVO.java
  42. 34 16
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/status/CrmBusinessStatusSaveReqVO.java
  43. 0 15
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/type/CrmBusinessStatusTypePageReqVO.java
  44. 0 19
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/type/CrmBusinessStatusTypeQueryVO.java
  45. 0 44
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/type/CrmBusinessStatusTypeRespVO.java
  46. 0 29
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/type/CrmBusinessStatusTypeSaveReqVO.java
  47. 75 9
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
  48. 6 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
  49. 64 50
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
  50. 41 37
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueSaveReqVO.java
  51. 0 17
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueTranslateReqVO.java
  52. 73 50
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/CrmContactController.java
  53. 22 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactBusiness2ReqVO.java
  54. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactBusinessReqVO.java
  55. 3 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactPageReqVO.java
  56. 55 45
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java
  57. 25 30
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactSaveReqVO.java
  58. 44 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractConfigController.java
  59. 103 34
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
  60. 16 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/config/CrmContractConfigRespVO.java
  61. 33 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/config/CrmContractConfigSaveReqVO.java
  62. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractPageReqVO.java
  63. 61 64
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java
  64. 28 32
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java
  65. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractTransferReqVO.java
  66. 0 16
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http
  67. 136 65
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
  68. 13 6
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
  69. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerDistributeReqVO.java
  70. 18 25
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java
  71. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportReqVO.java
  72. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportRespVO.java
  73. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerLockReqVO.java
  74. 20 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerPageReqVO.java
  75. 45 53
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java
  76. 25 32
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerSaveReqVO.java
  77. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerTransferReqVO.java
  78. 24 11
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java
  79. 12 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java
  80. 4 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/CrmOperateLogController.java
  81. 2 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java
  82. 30 10
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
  83. 36 17
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java
  84. 2 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java
  85. 4 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductSaveReqVO.java
  86. 64 29
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
  87. 67 33
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
  88. 0 54
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanBaseVO.java
  89. 0 12
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanCreateReqVO.java
  90. 4 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanPageReqVO.java
  91. 50 15
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java
  92. 45 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanSaveReqVO.java
  93. 0 18
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanUpdateReqVO.java
  94. 0 61
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableBaseVO.java
  95. 0 12
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableCreateReqVO.java
  96. 3 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivablePageReqVO.java
  97. 49 12
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java
  98. 58 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java
  99. 0 18
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableUpdateReqVO.java
  100. 9 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsRankController.http

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

@@ -257,11 +257,11 @@ public class CollectionUtils {
         return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
     }
 
-    public static <T> T findFirst(List<T> from, Predicate<T> predicate) {
+    public static <T> T findFirst(Collection<T> from, Predicate<T> predicate) {
         return findFirst(from, predicate, Function.identity());
     }
 
-    public static <T, U> U findFirst(List<T> from, Predicate<T> predicate, Function<T, U> func) {
+    public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) {
         if (CollUtil.isEmpty(from)) {
             return null;
         }

+ 36 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java

@@ -132,4 +132,40 @@ public class LocalDateTimeUtils {
         return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS);
     }
 
+    /**
+     * 获取今天的开始时间
+     *
+     * @return 今天
+     */
+    public static LocalDateTime getToday() {
+        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
+    }
+
+    /**
+     * 获取昨天的开始时间
+     *
+     * @return 昨天
+     */
+    public static LocalDateTime getYesterday() {
+        return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1));
+    }
+
+    /**
+     * 获取本月的开始时间
+     *
+     * @return 本月
+     */
+    public static LocalDateTime getMonth() {
+        return beginOfMonth(LocalDateTime.now());
+    }
+
+    /**
+     * 获取本年的开始时间
+     *
+     * @return 本年
+     */
+    public static LocalDateTime getYear() {
+        return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
+    }
+
 }

+ 59 - 7
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java

@@ -17,6 +17,7 @@ import java.util.Map;
 import java.util.function.Function;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.findFirst;
 
 /**
  * 区域工具类
@@ -70,10 +71,61 @@ public class AreaUtils {
      * @param id 区域编号
      * @return 区域
      */
-    public static Area getArea(Integer id) {
+    public static Area parseArea(Integer id) {
         return areas.get(id);
     }
 
+    /**
+     * 获得指定区域对应的编号
+     *
+     * @param pathStr 区域路径,例如说:河南省/石家庄市/新华区
+     * @return 区域
+     */
+    public static Area parseArea(String pathStr) {
+        String[] paths = pathStr.split("/");
+        Area area = null;
+        for (String path : paths) {
+            if (area == null) {
+                area = findFirst(areas.values(), item -> item.getName().equals(path));
+            } else {
+                area = findFirst(area.getChildren(), item -> item.getName().equals(path));
+            }
+        }
+        return area;
+    }
+
+    /**
+     * 获取所有节点的全路径名称如:河南省/石家庄市/新华区
+     *
+     * @param areas 地区树
+     * @return 所有节点的全路径名称
+     */
+    public static List<String> getAreaNodePathList(List<Area> areas) {
+        List<String> paths = new ArrayList<>();
+        areas.forEach(area -> getAreaNodePathList(area, "", paths));
+        return paths;
+    }
+
+    /**
+     * 构建一棵树的所有节点的全路径名称,并将其存储为 "祖先/父级/子级" 的形式
+     *
+     * @param node  父节点
+     * @param path  全路径名称
+     * @param paths 全路径名称列表,省份/城市/地区
+     */
+    private static void getAreaNodePathList(Area node, String path, List<String> paths) {
+        if (node == null) {
+            return;
+        }
+        // 构建当前节点的路径
+        String currentPath = path.isEmpty() ? node.getName() : path + "/" + node.getName();
+        paths.add(currentPath);
+        // 递归遍历子节点
+        for (Area child : node.getChildren()) {
+            getAreaNodePathList(child, currentPath, paths);
+        }
+    }
+
     /**
      * 格式化区域
      *
@@ -88,13 +140,13 @@ public class AreaUtils {
      * 格式化区域
      *
      * 例如说:
-     *      1. id = “静安区”时:上海 上海市 静安区
-     *      2. id = “上海市”时:上海 上海市
-     *      3. id = “上海”时:上海
-     *      4. id = “美国”时:美国
+     * 1. id = “静安区”时:上海 上海市 静安区
+     * 2. id = “上海市”时:上海 上海市
+     * 3. id = “上海”时:上海
+     * 4. id = “美国”时:美国
      * 当区域在中国时,默认不显示中国
      *
-     * @param id 区域编号
+     * @param id        区域编号
      * @param separator 分隔符
      * @return 格式化后的区域
      */
@@ -141,7 +193,7 @@ public class AreaUtils {
      */
     public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) {
         for (int i = 0; i < Byte.MAX_VALUE; i++) {
-            Area area = AreaUtils.getArea(id);
+            Area area = AreaUtils.parseArea(id);
             if (area == null) {
                 return null;
             }

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java

@@ -72,7 +72,7 @@ public class IPUtils {
      * @return 地区
      */
     public static Area getArea(String ip) {
-        return AreaUtils.getArea(getAreaId(ip));
+        return AreaUtils.parseArea(getAreaId(ip));
     }
 
     /**
@@ -82,6 +82,6 @@ public class IPUtils {
      * @return 地区
      */
     public static Area getArea(long ip) {
-        return AreaUtils.getArea(getAreaId(ip));
+        return AreaUtils.parseArea(getAreaId(ip));
     }
 }

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-biz-ip/src/test/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtilsTest.java

@@ -17,7 +17,7 @@ public class AreaUtilsTest {
     @Test
     public void testGetArea() {
         // 调用:北京
-        Area area = AreaUtils.getArea(110100);
+        Area area = AreaUtils.parseArea(110100);
         // 断言
         assertEquals(area.getId(), 110100);
         assertEquals(area.getName(), "北京市");

+ 5 - 0
yudao-framework/yudao-spring-boot-starter-excel/pom.xml

@@ -46,6 +46,11 @@
             <groupId>com.alibaba</groupId>
             <artifactId>easyexcel</artifactId>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
+            <optional>true</optional> <!-- 设置为 optional,只有在 AreaConvert 的时候使用 -->
+        </dependency>
     </dependencies>
 
 </project>

+ 46 - 0
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.framework.excel.core.convert;
+
+import cn.hutool.core.convert.Convert;
+import cn.iocoder.yudao.framework.ip.core.Area;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Excel 数据地区转换器
+ *
+ * @author HUIHUI
+ */
+@Slf4j
+public class AreaConvert implements Converter<Object> {
+
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        throw new UnsupportedOperationException("暂不支持,也不需要");
+    }
+
+    @Override
+    public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
+                                    GlobalConfiguration globalConfiguration) {
+        // 解析地区编号
+        String label = readCellData.getStringValue();
+        Area area = AreaUtils.parseArea(label);
+        if (area == null) {
+            log.error("[convertToJavaData][label({}) 解析不掉]", label);
+            return null;
+        }
+        // 将 value 转换成对应的属性
+        Class<?> fieldClazz = contentProperty.getField().getType();
+        return Convert.convert(fieldClazz, area.getId());
+    }
+
+}

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

@@ -0,0 +1,27 @@
+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;
+
+}

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

@@ -0,0 +1,115 @@
+package cn.iocoder.yudao.framework.excel.core.handler;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.excel.core.enums.ExcelColumn;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import org.apache.poi.hssf.usermodel.HSSFDataValidation;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddressList;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 基于固定 sheet 实现下拉框
+ *
+ * @author HUIHUI
+ */
+public class SelectSheetWriteHandler implements SheetWriteHandler {
+
+    /**
+     * 数据起始行从 0 开始
+     *
+     * 约定:本项目第一行有标题所以从 1 开始如果您的 Excel 有多行标题请自行更改
+     */
+    public static final int FIRST_ROW = 1;
+    /**
+     * 下拉列需要创建下拉框的行数,默认两千行如需更多请自行调整
+     */
+    public static final int LAST_ROW = 2000;
+
+    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;
+
+    public SelectSheetWriteHandler(List<KeyValue<ExcelColumn, List<String>>> selectMap) {
+        if (CollUtil.isEmpty(selectMap)) {
+            this.selectMap = null;
+            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;
+    }
+
+    @Override
+    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+        if (CollUtil.isEmpty(selectMap)) {
+            return;
+        }
+
+        // 1. 获取相应操作对象
+        DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手
+        Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿
+
+        // 2. 创建数据字典的 sheet 页
+        Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME);
+        for (KeyValue<ExcelColumn, List<String>> keyValue : selectMap) {
+            int rowLength = keyValue.getValue().size();
+            // 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));
+            }
+            // 2.2 设置单元格下拉选择
+            setColumnSelect(writeSheetHolder, workbook, helper, keyValue);
+        }
+    }
+
+    /**
+     * 设置单元格下拉选择
+     */
+    private static void setColumnSelect(WriteSheetHolder writeSheetHolder, Workbook workbook, DataValidationHelper helper,
+                                        KeyValue<ExcelColumn, List<String>> keyValue) {
+        // 1.1 创建可被其他单元格引用的名称
+        Name name = workbook.createName();
+        String excelColumn = keyValue.getKey().name();
+        // 1.2 下拉框数据来源 eg:字典sheet!$B1:$B2
+        String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + keyValue.getValue().size();
+        name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字
+        name.setRefersToFormula(refers); // 设置公式
+
+        // 2.1 设置约束
+        DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束
+        // 设置下拉单元格的首行、末行、首列、末列
+        CellRangeAddressList rangeAddressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW,
+                keyValue.getKey().getColNum(), keyValue.getKey().getColNum());
+        DataValidation validation = helper.createValidation(constraint, rangeAddressList);
+        if (validation instanceof HSSFDataValidation) {
+            validation.setSuppressDropDownArrow(false);
+        } else {
+            validation.setSuppressDropDownArrow(true);
+            validation.setShowErrorBox(true);
+        }
+        // 2.2 阻止输入非下拉框的值
+        validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
+        validation.createErrorBox("提示", "此值不存在于下拉选择中!");
+        // 2.3 添加下拉框约束
+        writeSheetHolder.getSheet().addValidationData(validation);
+    }
+
+}

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

@@ -1,11 +1,14 @@
 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;
 import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
+import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.web.multipart.MultipartFile;
 
-import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
@@ -21,29 +24,46 @@ public class ExcelUtils {
     /**
      * 将列表以 Excel 响应给前端
      *
-     * @param response 响应
-     * @param filename 文件名
+     * @param response  响应
+     * @param filename  文件名
      * @param sheetName Excel sheet 名
-     * @param head Excel head 头
-     * @param data 数据列表哦
-     * @param <T> 泛型,保证 head 和 data 类型的一致性
+     * @param head      Excel head 头
+     * @param data      数据列表哦
+     * @param <T>       泛型,保证 head 和 data 类型的一致性
      * @throws IOException 写入失败的情况
      */
     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 实现下拉框
                 .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
                 .sheet(sheetName).doWrite(data);
         // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
-        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
+        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
         response.setContentType("application/vnd.ms-excel;charset=UTF-8");
     }
 
     public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
-       return EasyExcel.read(file.getInputStream(), head, null)
+        return EasyExcel.read(file.getInputStream(), head, null)
                 .autoCloseStream(false)  // 不要自动关闭,交给 Servlet 自己处理
                 .doReadAllSync();
     }

+ 0 - 27
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/listener/BpmResultListenerApi.java

@@ -1,27 +0,0 @@
-package cn.iocoder.yudao.module.bpm.api.listener;
-
-import cn.iocoder.yudao.module.bpm.api.listener.dto.BpmResultListenerRespDTO;
-
-// TODO @芋艿:后续改成支持 RPC
-/**
- * 业务流程实例的结果发生变化的监听器 Api
- *
- * @author HUIHUI
- */
-public interface BpmResultListenerApi {
-
-    /**
-     * 监听的流程定义 Key
-     *
-     * @return 返回监听的流程定义 Key
-     */
-    String getProcessDefinitionKey();
-
-    /**
-     * 处理事件
-     *
-     * @param event 事件
-     */
-    void onEvent(BpmResultListenerRespDTO event);
-
-}

+ 0 - 32
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/listener/dto/BpmResultListenerRespDTO.java

@@ -1,32 +0,0 @@
-package cn.iocoder.yudao.module.bpm.api.listener.dto;
-
-import lombok.Data;
-
-// TODO @芋艿:后续改成支持 RPC
-/**
- * 业务流程实例的结果 Response DTO
- *
- * @author HUIHUI
- */
-@Data
-public class BpmResultListenerRespDTO {
-
-    /**
-     * 流程实例的编号
-     */
-    private String id;
-    /**
-     * 流程实例的 key
-     */
-    private String processDefinitionKey;
-    /**
-     * 流程实例的结果
-     */
-    private Integer result;
-    /**
-     * 流程实例对应的业务标识
-     * 例如说,请假
-     */
-    private String businessKey;
-
-}

+ 2 - 4
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEvent.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/event/BpmProcessInstanceResultEvent.java

@@ -1,11 +1,9 @@
-package cn.iocoder.yudao.module.bpm.framework.bpm.core.event;
+package cn.iocoder.yudao.module.bpm.event;
 
-import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 import org.springframework.context.ApplicationEvent;
 
-import jakarta.validation.constraints.NotNull;
-
 /**
  * 流程实例的结果发生变化的 Event
  * 定位:由于额外增加了 {@link BpmProcessInstanceExtDO#getResult()} 结果,所以增加该事件

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventListener.java → yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/event/BpmProcessInstanceResultEventListener.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.bpm.framework.bpm.core.event;
+package cn.iocoder.yudao.module.bpm.event;
 
 import cn.hutool.core.util.StrUtil;
 import org.springframework.context.ApplicationListener;

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java

@@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO;
-import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEvent;
+import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceResultEvent;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
 import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;

+ 1 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventPublisher.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.framework.bpm.core.event;
 
+import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceResultEvent;
 import lombok.AllArgsConstructor;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.validation.annotation.Validated;

+ 0 - 36
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/listener/BpmServiceResultListener.java

@@ -1,36 +0,0 @@
-package cn.iocoder.yudao.module.bpm.framework.bpm.listener;
-
-import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.bpm.api.listener.BpmResultListenerApi;
-import cn.iocoder.yudao.module.bpm.api.listener.dto.BpmResultListenerRespDTO;
-import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEvent;
-import jakarta.annotation.Resource;
-import org.springframework.context.ApplicationListener;
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-
-// TODO @芋艿:后续改成支持 RPC
-/**
- * 业务流程结果监听器实现类
- *
- * @author HUIHUI
- */
-@Component
-public class BpmServiceResultListener implements ApplicationListener<BpmProcessInstanceResultEvent> {
-
-    @Resource
-    private List<BpmResultListenerApi> bpmResultListenerApis;
-
-    @Override
-    public final void onApplicationEvent(BpmProcessInstanceResultEvent event) {
-        bpmResultListenerApis.forEach(bpmResultListenerApi -> {
-            if (!StrUtil.equals(event.getProcessDefinitionKey(), bpmResultListenerApi.getProcessDefinitionKey())) {
-                return;
-            }
-            bpmResultListenerApi.onEvent(BeanUtils.toBean(event, BpmResultListenerRespDTO.class));
-        });
-    }
-
-}

+ 2 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveResultListener.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.bpm.service.oa.listener;
 
-import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEvent;
-import cn.iocoder.yudao.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventListener;
+import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceResultEvent;
+import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceResultEventListener;
 import cn.iocoder.yudao.module.bpm.service.oa.BpmOALeaveService;
 import cn.iocoder.yudao.module.bpm.service.oa.BpmOALeaveServiceImpl;
 import org.springframework.stereotype.Component;

+ 1 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java

@@ -14,5 +14,6 @@ public interface DictTypeConstants {
     String CRM_PRODUCT_UNIT = "crm_product_unit"; // CRM 产品单位
     String CRM_PRODUCT_STATUS = "crm_product_status"; // CRM 产品状态
     String CRM_FOLLOW_UP_TYPE = "crm_follow_up_type"; // CRM 跟进方式
+    String CRM_RECEIVABLE_RETURN_TYPE = "crm_receivable_return_type"; // CRM 回款方式
 
 }

+ 27 - 21
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java

@@ -11,31 +11,40 @@ public interface ErrorCodeConstants {
 
     // ========== 合同管理 1-020-000-000 ==========
     ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
-    ErrorCode CONTRACT_UPDATE_FAIL_EDITING_PROHIBITED = new ErrorCode(1_020_000_001, "更新合同失败,原因:禁止编辑");
+    ErrorCode CONTRACT_UPDATE_FAIL_NOT_DRAFT = new ErrorCode(1_020_000_001, "合同更新失败,原因:合同不是草稿状态");
     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, "生成合同序列号重复,请重试");
 
     // ========== 线索管理 1-020-001-000 ==========
     ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
-    ErrorCode CLUE_ANY_CLUE_NOT_EXISTS = new ErrorCode(1_020_001_001, "线索【{}】不存在");
-    ErrorCode CLUE_ANY_CLUE_ALREADY_TRANSLATED = new ErrorCode(1_020_001_002, "线索【{}】已经转化过了,请勿重复转化");
+    ErrorCode CLUE_TRANSFORM_FAIL_ALREADY = new ErrorCode(1_020_001_001, "线索已经转化过了,请勿重复转化");
 
     // ========== 商机管理 1-020-002-000 ==========
     ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
-    ErrorCode BUSINESS_CONTRACT_EXISTS = new ErrorCode(1_020_002_001, "商机已关联合同,不能删除");
-
-    // TODO @lilleo:商机状态、商机类型,都单独错误码段
-
+    ErrorCode BUSINESS_DELETE_FAIL_CONTRACT_EXISTS = new ErrorCode(1_020_002_001, "商机已关联合同,不能删除");
+    ErrorCode BUSINESS_UPDATE_STATUS_FAIL_END_STATUS = new ErrorCode(1_020_002_002, "更新商机状态失败,原因:已经是结束状态");
+    ErrorCode BUSINESS_UPDATE_STATUS_FAIL_STATUS_EQUALS = new ErrorCode(1_020_002_003, "更新商机状态失败,原因:已经是该状态");
 
     // ========== 联系人管理 1-020-003-000 ==========
     ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
-    ErrorCode CONTACT_BUSINESS_LINK_NOT_EXISTS = new ErrorCode(1_020_003_001, "联系人商机关联不存在");
     ErrorCode CONTACT_DELETE_FAIL_CONTRACT_LINK_EXISTS = new ErrorCode(1_020_003_002, "联系人已关联合同,不能删除");
+    ErrorCode CONTACT_UPDATE_OWNER_USER_FAIL = new ErrorCode(1_020_003_003, "更新联系人负责人失败");
 
     // ========== 回款 1-020-004-000 ==========
     ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_020_004_000, "回款不存在");
-
-    // ========== 合同管理 1-020-005-000 ==========
+    ErrorCode RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED = new ErrorCode(1_020_004_001, "更新回款失败,原因:禁止编辑");
+    ErrorCode RECEIVABLE_DELETE_FAIL = new ErrorCode(1_020_004_002, "删除回款失败,原因: 被回款计划所使用,不允许删除");
+    ErrorCode RECEIVABLE_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_020_004_003, "回款提交审核失败,原因:回款没处在未提交状态");
+    ErrorCode RECEIVABLE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_004_004, "更新回款审核状态失败,原因:回款不是审核中状态");
+    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, "创建回款失败,原因:回款金额超出合同金额,目前剩余可退:{} 元");
+
+    // ========== 回款计划 1-020-005-000 ==========
     ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_020_005_000, "回款计划不存在");
+    ErrorCode RECEIVABLE_PLAN_UPDATE_FAIL = new ErrorCode(1_020_006_000, "更想回款计划失败,原因:已经有对应的还款");
+    ErrorCode RECEIVABLE_PLAN_EXISTS_RECEIVABLE = new ErrorCode(1_020_006_001, "回款计划已经有对应的回款,不能使用");
 
     // ========== 客户管理 1_020_006_000 ==========
     ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
@@ -53,14 +62,13 @@ public interface ErrorCodeConstants {
     ErrorCode CUSTOMER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_020_006_012, "导入客户数据不能为空!");
     ErrorCode CUSTOMER_CREATE_NAME_NOT_NULL = new ErrorCode(1_020_006_013, "客户名称不能为空!");
     ErrorCode CUSTOMER_NAME_EXISTS = new ErrorCode(1_020_006_014, "已存在名为【{}】的客户!");
+    ErrorCode CUSTOMER_UPDATE_DEAL_STATUS_FAIL = new ErrorCode(1_020_006_015, "更新客户的成交状态失败,原因:已经是该状态,无需更新");
 
     // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
     ErrorCode CRM_PERMISSION_DENIED = new ErrorCode(1_020_007_001, "{}操作失败,原因:没有权限");
-    ErrorCode CRM_PERMISSION_MODEL_NOT_EXISTS = new ErrorCode(1_020_007_002, "{}不存在");
     ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_007_003, "{}操作失败,原因:转移对象已经是该负责人");
     ErrorCode CRM_PERMISSION_DELETE_FAIL = new ErrorCode(1_020_007_004, "删除数据权限失败,原因:批量删除权限的时候,只能属于同一个 bizId 下");
-    ErrorCode CRM_PERMISSION_DELETE_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_005, "删除数据权限失败,原因:存在负责人");
     ErrorCode CRM_PERMISSION_DELETE_DENIED = new ErrorCode(1_020_007_006, "删除数据权限失败,原因:没有权限");
     ErrorCode CRM_PERMISSION_DELETE_SELF_PERMISSION_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_007, "删除数据权限失败,原因:不能删除负责人");
     ErrorCode CRM_PERMISSION_CREATE_FAIL = new ErrorCode(1_020_007_008, "创建数据权限失败,原因:所加用户已有权限");
@@ -68,6 +76,7 @@ public interface ErrorCodeConstants {
     // ========== 产品 1_020_008_000 ==========
     ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_020_008_000, "产品不存在");
     ErrorCode PRODUCT_NO_EXISTS = new ErrorCode(1_020_008_001, "产品编号已存在");
+    ErrorCode PRODUCT_NOT_ENABLE = new ErrorCode(1_020_008_002, "产品【{}】已禁用");
 
     // ========== 产品分类 1_020_009_000 ==========
     ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_020_009_000, "产品分类不存在");
@@ -77,21 +86,18 @@ public interface ErrorCodeConstants {
     ErrorCode PRODUCT_CATEGORY_PARENT_NOT_FIRST_LEVEL = new ErrorCode(1_020_009_004, "父分类不能是二级分类");
     ErrorCode product_CATEGORY_EXISTS_CHILDREN = new ErrorCode(1_020_009_005, "存在子分类,无法删除");
 
-    // ========== 商机状态类型 1_020_010_000 ==========
-    ErrorCode BUSINESS_STATUS_TYPE_NOT_EXISTS = new ErrorCode(1_020_010_000, "商机状态类型不存在");
-    ErrorCode BUSINESS_STATUS_TYPE_NAME_EXISTS = new ErrorCode(1_020_010_001, "商机状态类型名称已存在");
-
-    // ========== 商机状态 1_020_011_000 ==========
-    ErrorCode BUSINESS_STATUS_NOT_EXISTS = new ErrorCode(1_020_011_000, "商机状态不存在");
+    // ========== 商机状态 1_020_010_000 ==========
+    ErrorCode BUSINESS_STATUS_TYPE_NOT_EXISTS = new ErrorCode(1_020_010_000, "商机状态不存在");
+    ErrorCode BUSINESS_STATUS_TYPE_NAME_EXISTS = new ErrorCode(1_020_010_001, "商机状态组的名称已存在");
+    ErrorCode BUSINESS_STATUS_UPDATE_FAIL_USED = new ErrorCode(1_020_010_002, "已经被使用的商机状态组,无法进行更新");
+    ErrorCode BUSINESS_STATUS_DELETE_FAIL_USED = new ErrorCode(1_020_010_002, "已经被使用的商机状态组,无法进行删除");
+    ErrorCode BUSINESS_STATUS_NOT_EXISTS = new ErrorCode(1_020_010_003, "商机状态不存在");
 
     // ========== 客户公海规则设置 1_020_012_000 ==========
-    ErrorCode CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_020_012_000, "客户公海配置不存在或未启用");
     ErrorCode CUSTOMER_LIMIT_CONFIG_NOT_EXISTS = new ErrorCode(1_020_012_001, "客户限制配置不存在");
 
     // ========== 跟进记录 1_020_013_000 ==========
     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 ==========
-
 }

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

@@ -8,19 +8,21 @@ package cn.iocoder.yudao.module.crm.enums;
  */
 public interface LogRecordConstants {
 
-    // ======================= CRM_LEADS 线索 =======================
-
-    String CRM_LEADS_TYPE = "CRM 线索";
-    String CRM_LEADS_CREATE_SUB_TYPE = "创建线索";
-    String CRM_LEADS_CREATE_SUCCESS = "创建了线索{{#clue.name}}";
-    String CRM_LEADS_UPDATE_SUB_TYPE = "更新线索";
-    String CRM_LEADS_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReq}}";
-    String CRM_LEADS_DELETE_SUB_TYPE = "删除线索";
-    String CRM_LEADS_DELETE_SUCCESS = "删除了线索【{{#clueName}}】";
-    String CRM_LEADS_TRANSFER_SUB_TYPE = "转移线索";
-    String CRM_LEADS_TRANSFER_SUCCESS = "将线索【{{#clue.name}}】的负责人从【{getAdminUserById{#clue.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
-    String CRM_LEADS_TRANSLATE_SUB_TYPE = "线索转化为客户";
-    String CRM_LEADS_TRANSLATE_SUCCESS = "将线索【{{#clue.name}}】转化为客户";
+    // ======================= CRM_CLUE 线索 =======================
+
+    String CRM_CLUE_TYPE = "CRM 线索";
+    String CRM_CLUE_CREATE_SUB_TYPE = "创建线索";
+    String CRM_CLUE_CREATE_SUCCESS = "创建了线索{{#clue.name}}";
+    String CRM_CLUE_UPDATE_SUB_TYPE = "更新线索";
+    String CRM_CLUE_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReq}}";
+    String CRM_CLUE_DELETE_SUB_TYPE = "删除线索";
+    String CRM_CLUE_DELETE_SUCCESS = "删除了线索【{{#clueName}}】";
+    String CRM_CLUE_TRANSFER_SUB_TYPE = "转移线索";
+    String CRM_CLUE_TRANSFER_SUCCESS = "将线索【{{#clue.name}}】的负责人从【{getAdminUserById{#clue.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
+    String CRM_CLUE_TRANSLATE_SUB_TYPE = "线索转化为客户";
+    String CRM_CLUE_TRANSLATE_SUCCESS = "将线索【{{#clueName}}】转化为客户";
+    String CRM_CLUE_FOLLOW_UP_SUB_TYPE = "线索跟进";
+    String CRM_CLUE_FOLLOW_UP_SUCCESS = "线索跟进【{{#clueName}}】";
 
     // ======================= CRM_CUSTOMER 客户 =======================
 
@@ -41,6 +43,10 @@ public interface LogRecordConstants {
     String CRM_CUSTOMER_RECEIVE_SUCCESS = "{{#ownerUserName != null ? '将客户【' + #customer.name + '】分配给【' + #ownerUserName + '】' : '领取客户【' + #customer.name + '】'}}";
     String CRM_CUSTOMER_IMPORT_SUB_TYPE = "{{#isUpdate ? '导入并更新客户' : '导入客户'}}";
     String CRM_CUSTOMER_IMPORT_SUCCESS = "{{#isUpdate ? '导入并更新了客户【'+ #customer.name +'】' : '导入了客户【'+ #customer.name +'】'}}";
+    String CRM_CUSTOMER_UPDATE_DEAL_STATUS_SUB_TYPE = "更新客户成交状态";
+    String CRM_CUSTOMER_UPDATE_DEAL_STATUS_SUCCESS = "更新了客户【{{#customerName}}】的成交状态为【{{#dealStatus ? '已成交' : '未成交'}}】";
+    String CRM_CUSTOMER_FOLLOW_UP_SUB_TYPE = "客户跟进";
+    String CRM_CUSTOMER_FOLLOW_UP_SUCCESS = "客户跟进【{{#customerName}}】";
 
     // ======================= CRM_CUSTOMER_LIMIT_CONFIG 客户限制配置 =======================
 
@@ -69,6 +75,10 @@ public interface LogRecordConstants {
     String CRM_CONTACT_DELETE_SUCCESS = "删除了联系人【{{#contactName}}】";
     String CRM_CONTACT_TRANSFER_SUB_TYPE = "转移联系人";
     String CRM_CONTACT_TRANSFER_SUCCESS = "将联系人【{{#contact.name}}】的负责人从【{getAdminUserById{#contact.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
+    String CRM_CONTACT_FOLLOW_UP_SUB_TYPE = "联系人跟进";
+    String CRM_CONTACT_FOLLOW_UP_SUCCESS = "联系人跟进【{{#contactName}}】";
+    String CRM_CONTACT_UPDATE_OWNER_USER_SUB_TYPE = "更新联系人负责人";
+    String CRM_CONTACT_UPDATE_OWNER_USER_SUCCESS = "将联系人【{{#contact.name}}】的负责人从【{getAdminUserById{#contact.ownerUserId}}】变更为了【{getAdminUserById{#ownerUserId}}】";
 
     // ======================= CRM_BUSINESS 商机 =======================
 
@@ -81,6 +91,16 @@ public interface LogRecordConstants {
     String CRM_BUSINESS_DELETE_SUCCESS = "删除了商机【{{#businessName}}】";
     String CRM_BUSINESS_TRANSFER_SUB_TYPE = "转移商机";
     String CRM_BUSINESS_TRANSFER_SUCCESS = "将商机【{{#business.name}}】的负责人从【{getAdminUserById{#business.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
+    String CRM_BUSINESS_FOLLOW_UP_SUB_TYPE = "商机跟进";
+    String CRM_BUSINESS_FOLLOW_UP_SUCCESS = "商机跟进【{{#businessName}}】";
+    String CRM_BUSINESS_UPDATE_STATUS_SUB_TYPE = "更新商机状态";
+    String CRM_BUSINESS_UPDATE_STATUS_SUCCESS = "更新了商机【{{#businessName}}】的状态从【{{#oldStatusName}}】变更为了【{{#newStatusName}}】";
+
+    // ======================= CRM_CONTRACT_CONFIG 合同配置 =======================
+
+    String CRM_CONTRACT_CONFIG_TYPE = "CRM 合同配置";
+    String CRM_CONTRACT_CONFIG_SUB_TYPE = "{{#isPoolConfigUpdate ? '更新合同配置' : '创建合同配置'}}";
+    String CRM_CONTRACT_CONFIG_SUCCESS = "{{#isPoolConfigUpdate ? '更新了合同配置' : '创建了合同配置'}}";
 
     // ======================= CRM_CONTRACT 合同 =======================
 
@@ -95,6 +115,8 @@ public interface LogRecordConstants {
     String CRM_CONTRACT_TRANSFER_SUCCESS = "将合同【{{#contract.name}}】的负责人从【{getAdminUserById{#contract.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
     String CRM_CONTRACT_SUBMIT_SUB_TYPE = "提交合同审批";
     String CRM_CONTRACT_SUBMIT_SUCCESS = "提交合同【{{#contractName}}】审批成功";
+    String CRM_CONTRACT_FOLLOW_UP_SUB_TYPE = "合同跟进";
+    String CRM_CONTRACT_FOLLOW_UP_SUCCESS = "合同跟进【{{#contractName}}】";
 
     // ======================= CRM_PRODUCT 产品 =======================
 
@@ -125,6 +147,8 @@ public interface LogRecordConstants {
     String CRM_RECEIVABLE_UPDATE_SUCCESS = "更新了合同【{getContractById{#receivable.contractId}}】的第【{{#receivable.period}}】期回款: {_DIFF{#updateReqVO}}";
     String CRM_RECEIVABLE_DELETE_SUB_TYPE = "删除回款";
     String CRM_RECEIVABLE_DELETE_SUCCESS = "删除了合同【{getContractById{#receivable.contractId}}】的第【{{#receivable.period}}】期回款";
+    String CRM_RECEIVABLE_SUBMIT_SUB_TYPE = "提交回款审批";
+    String CRM_RECEIVABLE_SUBMIT_SUCCESS = "提交编号为【{{#receivableNo}}】的回款审批成功";
 
     // ======================= CRM_RECEIVABLE_PLAN 回款计划 =======================
 

+ 0 - 55
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/business/CrmBizEndStatus.java

@@ -1,55 +0,0 @@
-package cn.iocoder.yudao.module.crm.enums.business;
-
-import cn.hutool.core.util.ObjectUtil;
-import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-import java.util.Arrays;
-
-// TODO @lzxhqs:1)title、description、create 可以删除,非标准的 javadoc 注释哈,然后可以在类上加下这个类的注释;2)CrmBizEndStatus 改成 CrmBusinessEndStatus,非必要不缩写哈,可阅读比较重要
-/**
- * @author lzxhqs
- * @version 1.0
- * @title CrmBizEndStatus
- * @description
- * @create 2024/1/12
- */
-@RequiredArgsConstructor
-@Getter
-public enum CrmBizEndStatus implements IntArrayValuable {
-
-    WIN(1, "赢单"),
-    LOSE(2, "输单"),
-    INVALID(3, "无效");
-
-    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmBizEndStatus::getStatus).toArray();
-
-    // TODO @lzxhqs:这里的方法,建议放到 49 行之后;一般类里是,静态变量,普通变量;静态方法;普通方法
-    public static boolean isWin(Integer status) {
-        return ObjectUtil.equal(WIN.getStatus(), status);
-    }
-
-    public static boolean isLose(Integer status) {
-        return ObjectUtil.equal(LOSE.getStatus(), status);
-    }
-
-    public static boolean isInvalid(Integer status) {
-        return ObjectUtil.equal(INVALID.getStatus(), status);
-    }
-
-    /**
-     * 场景类型
-     */
-    private final Integer status;
-    /**
-     * 场景名称
-     */
-    private final String name;
-
-    @Override
-    public int[] array() {
-        return ARRAYS;
-    }
-
-}

+ 45 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/business/CrmBusinessEndStatusEnum.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.crm.enums.business;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 商机的结束状态枚举
+ *
+ * @author lzxhqs
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmBusinessEndStatusEnum implements IntArrayValuable {
+
+    WIN(1, "赢单"),
+    LOSE(2, "输单"),
+    INVALID(3, "无效");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmBusinessEndStatusEnum::getStatus).toArray();
+
+    /**
+     * 场景类型
+     */
+    private final Integer status;
+    /**
+     * 场景名称
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static CrmBusinessEndStatusEnum fromStatus(Integer status) {
+        return Arrays.stream(values())
+                .filter(value -> value.getStatus().equals(status))
+                .findFirst()
+                .orElse(null);
+    }
+
+}

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

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

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

@@ -10,6 +10,8 @@ import java.util.Arrays;
 /**
  * CRM 数据权限级别枚举
  *
+ * OWNER > WRITE > READ
+ *
  * @author HUIHUI
  */
 @Getter
@@ -17,8 +19,8 @@ import java.util.Arrays;
 public enum CrmPermissionLevelEnum implements IntArrayValuable {
 
     OWNER(1, "负责人"),
-    READ(2, "读"),
-    WRITE(3, "写");
+    READ(2, "读"),
+    WRITE(3, "写");
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmPermissionLevelEnum::getLevel).toArray();
 

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

@@ -1,27 +0,0 @@
-package cn.iocoder.yudao.module.crm.enums.permission;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-/**
- * Crm 数据权限角色枚举
- *
- * @author HUIHUI
- */
-@Getter
-@AllArgsConstructor
-public enum CrmPermissionRoleCodeEnum {
-
-    CRM_ADMIN("crm_admin", "CRM 管理员");
-
-    /**
-     * 角色标识
-     */
-    private String code;
-    /**
-     * 角色名称
-     */
-    private String name;
-
-}
-

+ 43 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/receivable/CrmReceivableReturnTypeEnum.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.crm.enums.receivable;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * CRM 回款方式枚举
+ *
+ * @author HUIHUI
+ */
+@Getter
+@AllArgsConstructor
+public enum CrmReceivableReturnTypeEnum implements IntArrayValuable {
+
+    CHECK(1, "支票"),
+    CASH(2, "现金"),
+    POSTAL_REMITTANCE(3, "邮政汇款"),
+    TELEGRAPHIC_TRANSFER(4, "电汇"),
+    ONLINE_TRANSFER(5, "网上转账"),
+    ALIPAY(6, "支付宝"),
+    WECHAT_PAY(7, "微信支付"),
+    OTHER(8, "其它");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmReceivableReturnTypeEnum::getType).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 名称
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 0 - 41
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/backlog/CrmBacklogController.java

@@ -1,41 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.backlog;
-
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.backlog.vo.CrmTodayCustomerPageReqVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.service.message.CrmBacklogService;
-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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-
-@Tag(name = "管理后台 - CRM待办消息")
-@RestController
-@RequestMapping("/crm/backlog")
-@Validated
-public class CrmBacklogController {
-
-    @Resource
-    private CrmBacklogService crmMessageService;
-
-    // TODO 芋艿:未来可能合并到 CrmCustomerController
-    @GetMapping("/today-customer-page")
-    @Operation(summary = "今日需联系客户")
-    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
-    public CommonResult<PageResult<CrmCustomerRespVO>> getTodayCustomerPage(@Valid CrmTodayCustomerPageReqVO pageReqVO) {
-        PageResult<CrmCustomerDO> pageResult = crmMessageService.getTodayCustomerPage(pageReqVO, getLoginUserId());
-        return success(BeanUtils.toBean(pageResult, CrmCustomerRespVO.class));
-    }
-
-}

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

@@ -1,37 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.backlog.vo;
-
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import cn.iocoder.yudao.framework.common.validation.InEnum;
-import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-@Schema(description = "管理后台 - 今日需联系客户 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmTodayCustomerPageReqVO extends PageParam {
-
-    /**
-     * 联系状态 - 今日需联系
-     */
-    public static final int CONTACT_TODAY = 1;
-    /**
-     * 联系状态 - 已逾期
-     */
-    public static final int CONTACT_EXPIRED = 2;
-    /**
-     * 联系状态 - 已联系
-     */
-    public static final int CONTACT_ALREADY = 3;
-
-    @Schema(description = "联系状态", example = "1")
-    private Integer contactStatus;
-
-    @Schema(description = "场景类型", example = "1")
-    @InEnum(CrmSceneTypeEnum.class)
-    private Integer sceneType;
-
-}

+ 0 - 9
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/bi/CrmBiRankController.http

@@ -1,9 +0,0 @@
-### 合同金额排行榜
-GET {{baseUrl}}/crm/bi-rank/get-contract-price-rank?deptId=100&times[0]=2022-12-12 00:00:00&times[1]=2024-12-12 23:59:59
-Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}
-
-### 回款金额排行榜
-GET {{baseUrl}}/crm/bi-rank/get-receivable-price-rank?deptId=100&times[0]=2022-12-12 00:00:00&times[1]=2024-12-12 23:59:59
-Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}

+ 0 - 32
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http

@@ -1,32 +0,0 @@
-### 请求 /transfer
-PUT {{baseUrl}}/crm/business/transfer
-Content-Type: application/json
-Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}
-
-{
-  "id": 1,
-  "ownerUserId": 2,
-  "transferType": 2,
-  "permissionType": 2
-}
-
-### 请求 /update
-PUT {{baseUrl}}/crm/business/update
-Content-Type: application/json
-Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}
-
-{
-  "id": 1,
-  "name": "2",
-  "statusTypeId": 2,
-  "statusId": 2,
-  "customerId": 1
-}
-
-### 请求 /get
-GET {{baseUrl}}/crm/business/get?id=1024
-Content-Type: application/json
-Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}

+ 83 - 38
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java

@@ -3,22 +3,26 @@ package cn.iocoder.yudao.module.crm.controller.admin.business;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+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.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessSaveReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
-import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusTypeDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductDO;
 import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
 import cn.iocoder.yudao.module.crm.service.business.CrmBusinessStatusService;
-import cn.iocoder.yudao.module.crm.service.business.CrmBusinessStatusTypeService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.crm.service.product.CrmProductService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -30,13 +34,15 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-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.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
@@ -52,9 +58,16 @@ public class CrmBusinessController {
     @Resource
     private CrmCustomerService customerService;
     @Resource
-    private CrmBusinessStatusTypeService businessStatusTypeService;
+    private CrmBusinessStatusService businessStatusTypeService;
     @Resource
     private CrmBusinessStatusService businessStatusService;
+    @Resource
+    private CrmProductService productService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建商机")
@@ -71,6 +84,14 @@ public class CrmBusinessController {
         return success(true);
     }
 
+    @PutMapping("/update-status")
+    @Operation(summary = "更新商机状态")
+    @PreAuthorize("@ss.hasPermission('crm:business:update')")
+    public CommonResult<Boolean> updateBusinessStatus(@Valid @RequestBody CrmBusinessUpdateStatusReqVO updateStatusReqVO) {
+        businessService.updateBusinessStatus(updateStatusReqVO);
+        return success(true);
+    }
+
     @DeleteMapping("/delete")
     @Operation(summary = "删除商机")
     @Parameter(name = "id", description = "编号", required = true)
@@ -86,15 +107,23 @@ public class CrmBusinessController {
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
     public CommonResult<CrmBusinessRespVO> getBusiness(@RequestParam("id") Long id) {
         CrmBusinessDO business = businessService.getBusiness(id);
-        return success(BeanUtils.toBean(business, CrmBusinessRespVO.class));
+        return success(buildBusinessDetail(business));
     }
 
-    @GetMapping("/list-by-ids")
-    @Operation(summary = "获得商机列表")
-    @Parameter(name = "ids", description = "编号", required = true, example = "[1024]")
-    @PreAuthorize("@ss.hasPermission('crm:business:query')")
-    public CommonResult<List<CrmBusinessRespVO>> getContactListByIds(@RequestParam("ids") List<Long> ids) {
-        return success(BeanUtils.toBean(businessService.getBusinessList(ids, getLoginUserId()), CrmBusinessRespVO.class));
+    private CrmBusinessRespVO buildBusinessDetail(CrmBusinessDO business) {
+        if (business == null) {
+            return null;
+        }
+        CrmBusinessRespVO businessVO = buildBusinessDetailList(Collections.singletonList(business)).get(0);
+        // 拼接产品项
+        List<CrmBusinessProductDO> businessProducts = businessService.getBusinessProductListByBusinessId(businessVO.getId());
+        Map<Long, CrmProductDO> productMap = productService.getProductMap(
+                convertSet(businessProducts, CrmBusinessProductDO::getProductId));
+        businessVO.setProducts(BeanUtils.toBean(businessProducts, CrmBusinessRespVO.Product.class, businessProductVO ->
+                MapUtils.findAndThen(productMap, businessProductVO.getProductId(),
+                        product -> businessProductVO.setProductName(product.getName())
+                                .setProductNo(product.getNo()).setProductUnit(product.getUnit()))));
+        return businessVO;
     }
 
     @GetMapping("/simple-all-list")
@@ -105,7 +134,8 @@ public class CrmBusinessController {
         reqVO.setPageSize(PAGE_SIZE_NONE); // 不分页
         PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(reqVO, getLoginUserId());
         return success(convertList(pageResult.getList(), business -> // 只返回 id、name 字段
-                new CrmBusinessRespVO().setId(business.getId()).setName(business.getName())));
+                new CrmBusinessRespVO().setId(business.getId()).setName(business.getName())
+                        .setCustomerId(business.getCustomerId())));
     }
 
     @GetMapping("/page")
@@ -113,7 +143,7 @@ public class CrmBusinessController {
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
     public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPage(@Valid CrmBusinessPageReqVO pageVO) {
         PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO, getLoginUserId());
-        return success(buildBusinessDetailPageResult(pageResult));
+        return success(new PageResult<>(buildBusinessDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/page-by-customer")
@@ -123,7 +153,7 @@ public class CrmBusinessController {
             throw exception(CUSTOMER_NOT_EXISTS);
         }
         PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPageByCustomerId(pageReqVO);
-        return success(buildBusinessDetailPageResult(pageResult));
+        return success(new PageResult<>(buildBusinessDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/page-by-contact")
@@ -131,7 +161,7 @@ public class CrmBusinessController {
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
     public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessContactPage(@Valid CrmBusinessPageReqVO pageReqVO) {
         PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPageByContact(pageReqVO);
-        return success(buildBusinessDetailPageResult(pageResult));
+        return success(new PageResult<>(buildBusinessDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/export-excel")
@@ -141,29 +171,44 @@ public class CrmBusinessController {
     public void exportBusinessExcel(@Valid CrmBusinessPageReqVO exportReqVO,
                                     HttpServletResponse response) throws IOException {
         exportReqVO.setPageSize(PAGE_SIZE_NONE);
-        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(exportReqVO, getLoginUserId());
+        List<CrmBusinessDO> list = businessService.getBusinessPage(exportReqVO, getLoginUserId()).getList();
         // 导出 Excel
         ExcelUtils.write(response, "商机.xls", "数据", CrmBusinessRespVO.class,
-                buildBusinessDetailPageResult(pageResult).getList());
+                buildBusinessDetailList(list));
     }
 
-    /**
-     * 构建详细的商机分页结果
-     *
-     * @param pageResult 简单的商机分页结果
-     * @return 详细的商机分页结果
-     */
-    private PageResult<CrmBusinessRespVO> buildBusinessDetailPageResult(PageResult<CrmBusinessDO> pageResult) {
-        if (CollUtil.isEmpty(pageResult.getList())) {
-            return PageResult.empty(pageResult.getTotal());
+    private List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
+        if (CollUtil.isEmpty(list)) {
+            return Collections.emptyList();
         }
-        List<CrmBusinessStatusTypeDO> statusTypeList = businessStatusTypeService.getBusinessStatusTypeList(
-                convertSet(pageResult.getList(), CrmBusinessDO::getStatusTypeId));
-        List<CrmBusinessStatusDO> statusList = businessStatusService.getBusinessStatusList(
-                convertSet(pageResult.getList(), CrmBusinessDO::getStatusId));
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(
-                convertSet(pageResult.getList(), CrmBusinessDO::getCustomerId));
-        return CrmBusinessConvert.INSTANCE.convertPage(pageResult, customerList, statusTypeList, statusList);
+        // 1.1 获取客户列表
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
+                convertSet(list, CrmBusinessDO::getCustomerId));
+        // 1.2 获取创建人、负责人列表
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(list,
+                contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        // 1.3 获得商机状态组
+        Map<Long, CrmBusinessStatusTypeDO> statusTypeMap = businessStatusTypeService.getBusinessStatusTypeMap(
+                convertSet(list, CrmBusinessDO::getStatusTypeId));
+        Map<Long, CrmBusinessStatusDO> statusMap = businessStatusService.getBusinessStatusMap(
+                convertSet(list, CrmBusinessDO::getStatusId));
+        // 2. 拼接数据
+        return BeanUtils.toBean(list, CrmBusinessRespVO.class, businessVO -> {
+            // 2.1 设置客户名称
+            MapUtils.findAndThen(customerMap, businessVO.getCustomerId(), customer -> businessVO.setCustomerName(customer.getName()));
+            // 2.2 设置创建人、负责人名称
+            MapUtils.findAndThen(userMap, NumberUtils.parseLong(businessVO.getCreator()),
+                    user -> businessVO.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, businessVO.getOwnerUserId(), user -> {
+                businessVO.setOwnerUserName(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> businessVO.setOwnerUserDeptName(dept.getName()));
+            });
+            // 2.3 设置商机状态
+            MapUtils.findAndThen(statusTypeMap, businessVO.getStatusTypeId(), statusType -> businessVO.setStatusTypeName(statusType.getName()));
+            MapUtils.findAndThen(statusMap, businessVO.getStatusId(), status -> businessVO.setStatusName(
+                    businessService.getBusinessStatusName(businessVO.getEndStatus(), status)));
+        });
     }
 
     @PutMapping("/transfer")

+ 126 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessStatusController.java

@@ -0,0 +1,126 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.status.CrmBusinessStatusRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.status.CrmBusinessStatusSaveReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusTypeDO;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessStatusService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import 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.*;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - CRM 商机状态")
+@RestController
+@RequestMapping("/crm/business-status")
+@Validated
+public class CrmBusinessStatusController {
+
+    @Resource
+    private CrmBusinessStatusService businessStatusTypeService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建商机状态")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:create')")
+    public CommonResult<Long> createBusinessStatus(@Valid @RequestBody CrmBusinessStatusSaveReqVO createReqVO) {
+        return success(businessStatusTypeService.createBusinessStatus(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新商机状态")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:update')")
+    public CommonResult<Boolean> updateBusinessStatus(@Valid @RequestBody CrmBusinessStatusSaveReqVO updateReqVO) {
+        businessStatusTypeService.updateBusinessStatus(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除商机状态")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:business-status:delete')")
+    public CommonResult<Boolean> deleteBusinessStatusType(@RequestParam("id") Long id) {
+        businessStatusTypeService.deleteBusinessStatusType(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得商机状态")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<CrmBusinessStatusRespVO> getBusinessStatusType(@RequestParam("id") Long id) {
+        CrmBusinessStatusTypeDO statusType = businessStatusTypeService.getBusinessStatusType(id);
+        if (statusType == null) {
+            return success(null);
+        }
+        List<CrmBusinessStatusDO> statuses = businessStatusTypeService.getBusinessStatusListByTypeId(id);
+        return success(BeanUtils.toBean(statusType, CrmBusinessStatusRespVO.class,
+                statusTypeVO -> statusTypeVO.setStatuses(BeanUtils.toBean(statuses, CrmBusinessStatusRespVO.Status.class))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商机状态分页")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<PageResult<CrmBusinessStatusRespVO>> getBusinessStatusPage(@Valid PageParam pageReqVO) {
+        // 1. 查询数据
+        PageResult<CrmBusinessStatusTypeDO> pageResult = businessStatusTypeService.getBusinessStatusTypePage(pageReqVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty(pageResult.getTotal()));
+        }
+        // 2. 拼接数据
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), statusType -> Long.parseLong(statusType.getCreator())));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+                convertSetByFlatMap(pageResult.getList(), CrmBusinessStatusTypeDO::getDeptIds, Collection::stream));
+        return success(BeanUtils.toBean(pageResult, CrmBusinessStatusRespVO.class, statusTypeVO -> {
+            statusTypeVO.setCreator(userMap.get(NumberUtils.parseLong(statusTypeVO.getCreator())).getNickname());
+            statusTypeVO.setDeptNames(convertList(statusTypeVO.getDeptIds(),
+                    deptId -> deptMap.containsKey(deptId) ? deptMap.get(deptId).getName() : null));
+        }));
+    }
+
+    @GetMapping("/type-simple-list")
+    @Operation(summary = "获得商机状态组列表")
+    public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusTypeSimpleList() {
+        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeList();
+        // 过滤掉部门不匹配的
+        Long deptId = adminUserApi.getUser(getLoginUserId()).getDeptId();
+        list.removeIf(statusType -> CollUtil.isNotEmpty(statusType.getDeptIds()) && !statusType.getDeptIds().contains(deptId));
+        return success(BeanUtils.toBean(list, CrmBusinessStatusRespVO.class));
+    }
+
+    @GetMapping("/status-simple-list")
+    @Operation(summary = "获得商机状态列表")
+    @Parameter(name = "typeId", description = "商机状态组", required = true, example = "1024")
+    public CommonResult<List<CrmBusinessStatusRespVO.Status>> getBusinessStatusSimpleList(@RequestParam("typeId") Long typeId) {
+        List<CrmBusinessStatusDO> list = businessStatusTypeService.getBusinessStatusListByTypeId(typeId);
+        return success(BeanUtils.toBean(list, CrmBusinessStatusRespVO.Status.class));
+    }
+
+}

+ 0 - 141
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessStatusTypeController.java

@@ -1,141 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business;
-
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.status.CrmBusinessStatusQueryVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.status.CrmBusinessStatusRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.type.CrmBusinessStatusTypePageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.type.CrmBusinessStatusTypeQueryVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.type.CrmBusinessStatusTypeRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.type.CrmBusinessStatusTypeSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessStatusConvert;
-import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessStatusTypeConvert;
-import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusTypeDO;
-import cn.iocoder.yudao.module.crm.service.business.CrmBusinessStatusService;
-import cn.iocoder.yudao.module.crm.service.business.CrmBusinessStatusTypeService;
-import cn.iocoder.yudao.module.system.api.dept.DeptApi;
-import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.annotation.Resource;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.Valid;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.*;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
-
-@Tag(name = "管理后台 - CRM 商机状态类型")
-@RestController
-@RequestMapping("/crm/business-status-type")
-@Validated
-public class CrmBusinessStatusTypeController {
-
-    @Resource
-    private CrmBusinessStatusTypeService businessStatusTypeService;
-
-    @Resource
-    private CrmBusinessStatusService businessStatusService;
-
-    @Resource
-    private DeptApi deptApi;
-
-    @PostMapping("/create")
-    @Operation(summary = "创建商机状态类型")
-    @PreAuthorize("@ss.hasPermission('crm:business-status-type:create')")
-    public CommonResult<Long> createBusinessStatusType(@Valid @RequestBody CrmBusinessStatusTypeSaveReqVO createReqVO) {
-        return success(businessStatusTypeService.createBusinessStatusType(createReqVO));
-    }
-
-    @PutMapping("/update")
-    @Operation(summary = "更新商机状态类型")
-    @PreAuthorize("@ss.hasPermission('crm:business-status-type:update')")
-    public CommonResult<Boolean> updateBusinessStatusType(@Valid @RequestBody CrmBusinessStatusTypeSaveReqVO updateReqVO) {
-        businessStatusTypeService.updateBusinessStatusType(updateReqVO);
-        return success(true);
-    }
-
-    @DeleteMapping("/delete")
-    @Operation(summary = "删除商机状态类型")
-    @Parameter(name = "id", description = "编号", required = true)
-    @PreAuthorize("@ss.hasPermission('crm:business-status-type:delete')")
-    public CommonResult<Boolean> deleteBusinessStatusType(@RequestParam("id") Long id) {
-        businessStatusTypeService.deleteBusinessStatusType(id);
-        return success(true);
-    }
-
-    @GetMapping("/get")
-    @Operation(summary = "获得商机状态类型")
-    @Parameter(name = "id", description = "编号", required = true, example = "1024")
-    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
-    public CommonResult<CrmBusinessStatusTypeRespVO> getBusinessStatusType(@RequestParam("id") Long id) {
-        CrmBusinessStatusTypeDO statusType = businessStatusTypeService.getBusinessStatusType(id);
-        // 处理状态回显
-        // TODO @lzxhqs:可以在 businessStatusService 加个 getBusinessStatusListByTypeId 方法,直接返回 List<CrmBusinessStatusDO> 哈,常用的,尽量封装个简单易懂的方法,不用追求绝对通用哈;
-        CrmBusinessStatusQueryVO queryVO = new CrmBusinessStatusQueryVO();
-        queryVO.setTypeId(id);
-        List<CrmBusinessStatusDO> statusList = businessStatusService.selectList(queryVO);
-        return success(CrmBusinessStatusTypeConvert.INSTANCE.convert(statusType, statusList));
-    }
-
-    @GetMapping("/page")
-    @Operation(summary = "获得商机状态类型分页")
-    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
-    public CommonResult<PageResult<CrmBusinessStatusTypeRespVO>> getBusinessStatusTypePage(@Valid CrmBusinessStatusTypePageReqVO pageReqVO) {
-        PageResult<CrmBusinessStatusTypeDO> pageResult = businessStatusTypeService.getBusinessStatusTypePage(pageReqVO);
-        // 处理部门回显
-        Set<Long> deptIds = CollectionUtils.convertSetByFlatMap(pageResult.getList(), CrmBusinessStatusTypeDO::getDeptIds,Collection::stream);
-        List<DeptRespDTO> deptList = deptApi.getDeptList(deptIds);
-        return success(CrmBusinessStatusTypeConvert.INSTANCE.convertPage(pageResult, deptList));
-    }
-
-    @GetMapping("/export-excel")
-    @Operation(summary = "导出商机状态类型 Excel")
-    @PreAuthorize("@ss.hasPermission('crm:business-status-type:export')")
-    @OperateLog(type = EXPORT)
-    public void exportBusinessStatusTypeExcel(@Valid CrmBusinessStatusTypePageReqVO pageReqVO,
-              HttpServletResponse response) throws IOException {
-        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
-        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypePage(pageReqVO).getList();
-        // 导出 Excel
-        ExcelUtils.write(response, "商机状态类型.xls", "数据", CrmBusinessStatusTypeRespVO.class,
-                        BeanUtils.toBean(list, CrmBusinessStatusTypeRespVO.class));
-    }
-
-    @GetMapping("/get-simple-list")
-    @Operation(summary = "获得商机状态类型列表")
-    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
-    public CommonResult<List<CrmBusinessStatusTypeRespVO>> getBusinessStatusTypeList() {
-        CrmBusinessStatusTypeQueryVO queryVO = new CrmBusinessStatusTypeQueryVO();
-        queryVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
-        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.selectList(queryVO);
-        return success(BeanUtils.toBean(list, CrmBusinessStatusTypeRespVO.class));
-    }
-
-    // TODO @ljlleo 这个接口,是不是可以和 getBusinessStatusTypeList 合并成一个?
-    @GetMapping("/get-status-list")
-    @Operation(summary = "获得商机状态列表")
-    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
-    public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusListByTypeId(@RequestParam("typeId") Long typeId) {
-        CrmBusinessStatusQueryVO queryVO = new CrmBusinessStatusQueryVO();
-        queryVO.setTypeId(typeId);
-        List<CrmBusinessStatusDO> list = businessStatusService.selectList(queryVO);
-        return success(CrmBusinessStatusConvert.INSTANCE.convertList(list));
-    }
-
-}

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

@@ -1,75 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business;
-
-import com.alibaba.excel.annotation.ExcelProperty;
-import lombok.Data;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.Set;
-
-/**
- * 商机 Excel VO
- *
- * @author ljlleo
- */
-@Data
-public class CrmBusinessExcelVO {
-
-    @ExcelProperty("主键")
-    private Long id;
-
-    @ExcelProperty("商机名称")
-    private String name;
-
-    @ExcelProperty("商机状态类型编号")
-    private Long statusTypeId;
-
-    @ExcelProperty("商机状态编号")
-    private Long statusId;
-
-    @ExcelProperty("下次联系时间")
-    private LocalDateTime contactNextTime;
-
-    @ExcelProperty("客户编号")
-    private Long customerId;
-
-    @ExcelProperty("预计成交日期")
-    private LocalDateTime dealTime;
-
-    @ExcelProperty("商机金额")
-    private BigDecimal price;
-
-    @ExcelProperty("整单折扣")
-    private BigDecimal discountPercent;
-
-    @ExcelProperty("产品总金额")
-    private BigDecimal productPrice;
-
-    @ExcelProperty("备注")
-    private String remark;
-
-    @ExcelProperty("负责人的用户编号")
-    private Long ownerUserId;
-
-    @ExcelProperty("创建时间")
-    private LocalDateTime createTime;
-
-    @ExcelProperty("只读权限的用户编号数组")
-    private Set<Long> roUserIds;
-
-    @ExcelProperty("读写权限的用户编号数组")
-    private Set<Long> rwUserIds;
-
-    @ExcelProperty("1赢单2输单3无效")
-    private Integer endStatus;
-
-    @ExcelProperty("结束时的备注")
-    private String endRemark;
-
-    @ExcelProperty("最后跟进时间")
-    private LocalDateTime contactLastTime;
-
-    @ExcelProperty("跟进状态")
-    private Integer followUpStatus;
-
-}

+ 104 - 29
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java

@@ -1,69 +1,144 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business;
 
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
 import lombok.Data;
-import org.springframework.format.annotation.DateTimeFormat;
+import lombok.NoArgsConstructor;
 
 import java.math.BigDecimal;
 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 = "管理后台 - 商机 Response VO")
+@Schema(description = "管理后台 - CRM 商机 Response VO")
 @Data
+@ExcelIgnoreUnannotated
 public class CrmBusinessRespVO {
 
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    @ExcelProperty("编号")
     private Long id;
 
     @Schema(description = "商机名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
-    @NotNull(message = "商机名称不能为空")
+    @ExcelProperty("商机名称")
     private String name;
 
-    @Schema(description = "商机状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714")
-    @NotNull(message = "商机状态类型不能为空")
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10299")
+    private Long customerId;
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @ExcelProperty("客户名称")
+    private String customerName;
+
+    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example ="true")
+    @ExcelProperty("跟进状态")
+    private Boolean followUpStatus;
+
+    @Schema(description = "最后跟进时间")
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "下次联系时间")
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @Schema(description = "负责人的用户编号", example = "25682")
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+    @Schema(description = "负责人名字", example = "25682")
+    @ExcelProperty("负责人名字")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    @ExcelProperty("负责人部门")
+    private String ownerUserDeptName;
+
+    @Schema(description = "商机状态组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714")
     private Long statusTypeId;
+    @Schema(description = "商机状组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中")
+    @ExcelProperty("商机状态组")
+    private String statusTypeName;
 
     @Schema(description = "商机状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30320")
-    @NotNull(message = "商机状态不能为空")
     private Long statusId;
+    @Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "跟进中")
+    @ExcelProperty("商机状态")
+    private String statusName;
 
-    @Schema(description = "下次联系时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime contactNextTime;
+    @Schema
+    @ExcelProperty("结束状态")
+    private Integer endStatus;
 
-    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10299")
-    @NotNull(message = "客户不能为空")
-    private Long customerId;
+    @ExcelProperty("结束时的备注")
+    private String endRemark;
 
     @Schema(description = "预计成交日期")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ExcelProperty("预计成交日期")
     private LocalDateTime dealTime;
 
-    @Schema(description = "商机金额", example = "12371")
-    private Integer price;
+    @Schema(description = "产品总金额", example = "12025")
+    @ExcelProperty("产品总金额")
+    private BigDecimal totalProductPrice;
 
-    // TODO @ljileo:折扣使用 Integer 类型,存储时,默认 * 100;展示的时候,前端需要 / 100;避免精度丢失问题
     @Schema(description = "整单折扣")
-    private Integer discountPercent;
+    @ExcelProperty("整单折扣")
+    private BigDecimal discountPercent;
 
-    @Schema(description = "产品总金额", example = "12025")
-    private BigDecimal productPrice;
+    @Schema(description = "商机总金额", example = "12371")
+    @ExcelProperty("商机总金额")
+    private BigDecimal totalPrice;
 
     @Schema(description = "备注", example = "随便")
+    @ExcelProperty("备注")
     private String remark;
 
+    @Schema(description = "创建人", example = "1024")
+    @ExcelProperty("创建人")
+    private String creator;
+    @Schema(description = "创建人名字", example = "芋道源码")
+    @ExcelProperty("创建人名字")
+    private String creatorName;
+
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
     private LocalDateTime createTime;
 
-    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
-    private String customerName;
+    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("更新时间")
+    private LocalDateTime updateTime;
 
-    @Schema(description = "状态类型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中")
-    private String statusTypeName;
+    @Schema(description = "产品列表")
+    private List<Product> products;
 
-    @Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "跟进中")
-    private String statusName;
+    @Schema(description = "产品列表")
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class Product {
+
+        @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888")
+        private Long id;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+        private Long productId;
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+        private String productNo;
+        @Schema(description = "产品单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+        private Integer productUnit;
+
+        @Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "商机价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        private BigDecimal businessPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
+        private BigDecimal count;
+
+        @Schema(description = "总计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        private BigDecimal totalPrice;
+
+    }
 
 }

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

@@ -1,8 +1,7 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business;
 
-import cn.iocoder.yudao.framework.common.validation.InEnum;
-import cn.iocoder.yudao.module.crm.enums.business.CrmBizEndStatus;
 import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerParseFunction;
+import cn.iocoder.yudao.module.crm.framework.operatelog.core.SysAdminUserParseFunction;
 import com.mzt.logapi.starter.annotation.DiffLogField;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
@@ -29,75 +28,68 @@ public class CrmBusinessSaveReqVO {
     @NotNull(message = "商机名称不能为空")
     private String name;
 
-    @Schema(description = "商机状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714")
-    @DiffLogField(name = "商机状态")
-    @NotNull(message = "商机状态类型不能为空")
-    private Long statusTypeId;
-
-    @Schema(description = "商机状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30320")
-    @DiffLogField(name = "商机状态")
-    @NotNull(message = "商机状态不能为空")
-    private Long statusId;
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10299")
+    @DiffLogField(name = "客户", function = CrmCustomerParseFunction.NAME)
+    @NotNull(message = "客户不能为空")
+    private Long customerId;
 
     @Schema(description = "下次联系时间")
     @DiffLogField(name = "下次联系时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactNextTime;
 
-    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10299")
-    @DiffLogField(name = "客户", function = CrmCustomerParseFunction.NAME)
-    @NotNull(message = "客户不能为空")
-    private Long customerId;
+    @Schema(description = "负责人用户编号", example = "14334")
+    @NotNull(message = "负责人不能为空")
+    @DiffLogField(name = "负责人", function = SysAdminUserParseFunction.NAME)
+    private Long ownerUserId;
+
+    @Schema(description = "商机状态组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714")
+    @DiffLogField(name = "商机状态组")
+    @NotNull(message = "商机状态组不能为空")
+    private Long statusTypeId;
 
     @Schema(description = "预计成交日期")
     @DiffLogField(name = "预计成交日期")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime dealTime;
 
-    @Schema(description = "商机金额", example = "12371")
-    @DiffLogField(name = "商机金额")
-    private Integer price;
-
-    @Schema(description = "整单折扣")
+    @Schema(description = "整单折扣", requiredMode = Schema.RequiredMode.REQUIRED, example = "55.00")
     @DiffLogField(name = "整单折扣")
-    private Integer discountPercent;
-
-    @Schema(description = "产品总金额", example = "12025")
-    @DiffLogField(name = "产品总金额")
-    private BigDecimal productPrice;
+    @NotNull(message = "整单折扣不能为空")
+    private BigDecimal discountPercent;
 
     @Schema(description = "备注", example = "随便")
     @DiffLogField(name = "备注")
     private String remark;
 
-    @Schema(description = "结束状态", example = "1")
-    @InEnum(CrmBizEndStatus.class)
-    private Integer endStatus;
-
     @Schema(description = "联系人编号", example = "110")
     private Long contactId; // 使用场景,在【联系人详情】添加商机时,如果需要关联两者,需要传递 contactId 字段
 
-    // TODO @puhui999:传递 items 就行啦;
     @Schema(description = "产品列表")
-    private List<CrmBusinessProductItem> productItems;
+    private List<Product> products;
 
     @Schema(description = "产品列表")
     @Data
     @NoArgsConstructor
     @AllArgsConstructor
-    public static class CrmBusinessProductItem {
+    public static class Product {
 
         @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
         @NotNull(message = "产品编号不能为空")
-        private Long id;
+        private Long productId;
+
+        @Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        @NotNull(message = "产品单价不能为空")
+        private BigDecimal productPrice;
+
+        @Schema(description = "商机价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        @NotNull(message = "商机价格不能为空")
+        private BigDecimal businessPrice;
 
         @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
         @NotNull(message = "产品数量不能为空")
         private Integer count;
 
-        @Schema(description = "产品折扣")
-        private Integer discountPercent;
-
     }
 
 }

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

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.business.CrmBusinessEndStatusEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 商机更新状态 Request VO")
+@Data
+public class CrmBusinessUpdateStatusReqVO {
+
+    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    @NotNull(message = "商机编号不能为空")
+    private Long id;
+
+    @Schema(description = "状态编号", example = "1")
+    private Long statusId;
+
+    @Schema(description = "结束状态", example = "1")
+    @InEnum(value = CrmBusinessEndStatusEnum.class)
+    private Integer endStatus;
+
+    @AssertTrue(message = "变更状态不正确")
+    public boolean isStatusValid() {
+        return statusId != null || endStatus != null;
+    }
+
+}

+ 0 - 15
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/status/CrmBusinessStatusPageReqVO.java

@@ -1,15 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business.vo.status;
-
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-@Schema(description = "管理后台 - 商机状态分页 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmBusinessStatusPageReqVO extends PageParam {
-
-}

+ 0 - 19
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/status/CrmBusinessStatusQueryVO.java

@@ -1,19 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business.vo.status;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.ToString;
-
-import java.util.Collection;
-
-@Schema(description = "管理后台 - 商机状态 Query VO")
-@Data
-@ToString(callSuper = true)
-public class CrmBusinessStatusQueryVO {
-
-    @Schema(description = "主键集合")
-    private Collection<Long> idList;
-
-    @Schema(description = "状态类型编号")
-    private Long typeId;
-}

+ 35 - 17
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/status/CrmBusinessStatusRespVO.java

@@ -1,33 +1,51 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo.status;
 
-import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
-import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
 @Schema(description = "管理后台 - 商机状态 Response VO")
 @Data
-@ExcelIgnoreUnannotated
 public class CrmBusinessStatusRespVO {
 
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "23899")
-    @ExcelProperty("主键")
+    @Schema(description = "状态组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2934")
     private Long id;
 
-    @Schema(description = "状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7139")
-    @ExcelProperty("状态类型编号")
-    private Long typeId;
-
-    @Schema(description = "状态名", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
-    @ExcelProperty("状态名")
+    @Schema(description = "状态组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
     private String name;
 
-    @Schema(description = "赢单率")
-    @ExcelProperty("赢单率")
-    private String percent;
+    @Schema(description = "使用的部门编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Long> deptIds;
+    @Schema(description = "使用的部门名称", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<String> deptNames;
+
+    @Schema(description = "创建人", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String creator;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+    @Schema(description = "状态集合", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Status> statuses;
+
+    @Data
+    public static class Status {
+
+        @Schema(description = "状态编号", example = "23899")
+        private Long id;
+
+        @Schema(description = "状态名", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
+        private String name;
+
+        @Schema(description = "赢单率", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
+        private BigDecimal percent;
+
+        @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Integer sort;
 
-    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    @ExcelProperty("排序")
-    private Integer sort;
+    }
 
 }

+ 34 - 16
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/status/CrmBusinessStatusSaveReqVO.java

@@ -1,32 +1,50 @@
 package cn.iocoder.yudao.module.crm.controller.admin.business.vo.status;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
+import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
+import lombok.Data;
 
-@Schema(description = "管理后台 - 商机状态新增/修改 Request VO")
+import java.math.BigDecimal;
+import java.util.List;
+
+@Schema(description = "管理后台 - 商机状态组新增/修改 Request VO")
 @Data
 public class CrmBusinessStatusSaveReqVO {
 
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "23899")
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "2934")
     private Long id;
 
-    @Schema(description = "状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7139")
-    @NotNull(message = "状态类型编号不能为空")
-    private Long typeId;
-
-    @Schema(description = "状态名", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
-    @NotEmpty(message = "状态名不能为空")
+    @Schema(description = "状态类型名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotEmpty(message = "状态类型名不能为空")
     private String name;
 
-    // TODO @lzxhqs::percent 应该是 Integer;
-    @Schema(description = "赢单率")
-    private String percent;
+    @Schema(description = "使用的部门编号")
+    private List<Long> deptIds;
+
+    @Schema(description = "商机状态集合", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "商机状态集合不能为空")
+    @Valid
+    private List<Status> statuses;
+
+    @Data
+    public static class Status {
+
+        @Schema(description = "状态编号", example = "23899")
+        private Long id;
+
+        @Schema(description = "状态名", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
+        @NotEmpty(message = "状态名不能为空")
+        private String name;
+
+        @Schema(description = "赢单率", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
+        @NotNull(message = "赢单率不能为空")
+        private BigDecimal percent;
+
+        @Schema(description = "排序", hidden = true, example = "1")
+        private Integer sort;
 
-    // TODO @lzxhqs:这个是不是不用前端新增和修改的时候传递,交给顺序计算出来,存储起来就好了;
-    @Schema(description = "排序")
-    private Integer sort;
+    }
 
 }

+ 0 - 15
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/type/CrmBusinessStatusTypePageReqVO.java

@@ -1,15 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business.vo.type;
-
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-@Schema(description = "管理后台 - 商机状态类型分页 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmBusinessStatusTypePageReqVO extends PageParam {
-
-}

+ 0 - 19
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/type/CrmBusinessStatusTypeQueryVO.java

@@ -1,19 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business.vo.type;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import lombok.ToString;
-
-import java.util.Collection;
-
-@Schema(description = "管理后台 - 商机状态类型 Query VO")
-@Data
-@ToString(callSuper = true)
-public class CrmBusinessStatusTypeQueryVO {
-
-    @Schema(description = "主键集合")
-    private Collection<Long> idList;
-
-    @Schema(description = "状态")
-    private Integer status;
-}

+ 0 - 44
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/type/CrmBusinessStatusTypeRespVO.java

@@ -1,44 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business.vo.type;
-
-import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
-import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
-import com.alibaba.excel.annotation.ExcelProperty;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import java.time.LocalDateTime;
-import java.util.List;
-
-@Schema(description = "管理后台 - 商机状态类型 Response VO")
-@Data
-@ExcelIgnoreUnannotated
-public class CrmBusinessStatusTypeRespVO {
-
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "2934")
-    @ExcelProperty("主键")
-    private Long id;
-
-    @Schema(description = "状态类型名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
-    @ExcelProperty("状态类型名")
-    private String name;
-
-    @Schema(description = "使用的部门编号", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("使用的部门编号")
-    private List<Long> deptIds;
-    @Schema(description = "使用的部门名称", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("使用的部门名称")
-    private List<String> deptNames;
-
-    @Schema(description = "创建人", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("创建人")
-    private String creator;
-
-    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("创建时间")
-    private LocalDateTime createTime;
-
-    // TODO @ljlleo 字段后缀改成 statuses,保持和 deptIds 风格一致;CrmBusinessStatusDO 改成 VO 哈;一般不使用 do 直接返回
-    @Schema(description = "状态集合", requiredMode = Schema.RequiredMode.REQUIRED)
-    private List<CrmBusinessStatusDO> statusList;
-
-}

+ 0 - 29
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/type/CrmBusinessStatusTypeSaveReqVO.java

@@ -1,29 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.business.vo.type;
-
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.status.CrmBusinessStatusSaveReqVO;
-import com.google.common.collect.Lists;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import jakarta.validation.constraints.NotEmpty;
-import java.util.List;
-
-@Schema(description = "管理后台 - 商机状态类型新增/修改 Request VO")
-@Data
-public class CrmBusinessStatusTypeSaveReqVO {
-
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "2934")
-    private Long id;
-
-    @Schema(description = "状态类型名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
-    @NotEmpty(message = "状态类型名不能为空")
-    private String name;
-
-    // TODO @lzxhqs: VO 里面,我们不使用默认值哈。这里 Lists.newArrayList() 看看怎么去掉。上面 deptIds 也是类似噢
-    @Schema(description = "使用的部门编号", requiredMode = Schema.RequiredMode.REQUIRED)
-    private List<Long> deptIds = Lists.newArrayList();
-
-    @Schema(description = "商机状态集合", requiredMode = Schema.RequiredMode.REQUIRED)
-    private List<CrmBusinessStatusSaveReqVO> statusList;
-
-}

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

@@ -1,13 +1,26 @@
 package cn.iocoder.yudao.module.crm.controller.admin.clue;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+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.util.ExcelUtils;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmCluePageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.CrmClueTransferReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.service.clue.CrmClueService;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -19,12 +32,18 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static java.util.Collections.singletonList;
 
 @Tag(name = "管理后台 - 线索")
 @RestController
@@ -34,12 +53,19 @@ public class CrmClueController {
 
     @Resource
     private CrmClueService clueService;
+    @Resource
+    private CrmCustomerService customerService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建线索")
     @PreAuthorize("@ss.hasPermission('crm:clue:create')")
     public CommonResult<Long> createClue(@Valid @RequestBody CrmClueSaveReqVO createReqVO) {
-        return success(clueService.createClue(createReqVO, getLoginUserId()));
+        return success(clueService.createClue(createReqVO));
     }
 
     @PutMapping("/update")
@@ -65,7 +91,14 @@ public class CrmClueController {
     @PreAuthorize("@ss.hasPermission('crm:clue:query')")
     public CommonResult<CrmClueRespVO> getClue(@RequestParam("id") Long id) {
         CrmClueDO clue = clueService.getClue(id);
-        return success(BeanUtils.toBean(clue, CrmClueRespVO.class));
+        return success(buildClueDetail(clue));
+    }
+
+    private CrmClueRespVO buildClueDetail(CrmClueDO clue) {
+        if (clue == null) {
+            return null;
+        }
+        return buildClueDetailList(singletonList(clue)).get(0);
     }
 
     @GetMapping("/page")
@@ -73,7 +106,7 @@ public class CrmClueController {
     @PreAuthorize("@ss.hasPermission('crm:clue:query')")
     public CommonResult<PageResult<CrmClueRespVO>> getCluePage(@Valid CrmCluePageReqVO pageVO) {
         PageResult<CrmClueDO> pageResult = clueService.getCluePage(pageVO, getLoginUserId());
-        return success(BeanUtils.toBean(pageResult, CrmClueRespVO.class));
+        return success(new PageResult<>(buildClueDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/export-excel")
@@ -84,8 +117,33 @@ public class CrmClueController {
         pageReqVO.setPageSize(PAGE_SIZE_NONE);
         List<CrmClueDO> list = clueService.getCluePage(pageReqVO, getLoginUserId()).getList();
         // 导出 Excel
-        List<CrmClueRespVO> datas = BeanUtils.toBean(list, CrmClueRespVO.class);
-        ExcelUtils.write(response, "线索.xls", "数据", CrmClueRespVO.class, datas);
+        ExcelUtils.write(response, "线索.xls", "数据", CrmClueRespVO.class, buildClueDetailList(list));
+    }
+
+    private List<CrmClueRespVO> buildClueDetailList(List<CrmClueDO> list) {
+        if (CollUtil.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        // 1.1 获取客户列表
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
+                convertSet(list, CrmClueDO::getCustomerId));
+        // 1.2 获取创建人、负责人列表
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(list,
+                contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        // 2. 转换成 VO
+        return BeanUtils.toBean(list, CrmClueRespVO.class, clueVO -> {
+            clueVO.setAreaName(AreaUtils.format(clueVO.getAreaId()));
+            // 2.1 设置客户名称
+            MapUtils.findAndThen(customerMap, clueVO.getCustomerId(), customer -> clueVO.setCustomerName(customer.getName()));
+            // 2.2 设置创建人、负责人名称
+            MapUtils.findAndThen(userMap, NumberUtils.parseLong(clueVO.getCreator()),
+                    user -> clueVO.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, clueVO.getOwnerUserId(), user -> {
+                clueVO.setOwnerUserName(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> clueVO.setOwnerUserDeptName(dept.getName()));
+            });
+        });
     }
 
     @PutMapping("/transfer")
@@ -96,12 +154,20 @@ public class CrmClueController {
         return success(true);
     }
 
-    @PostMapping("/transform")
+    @PutMapping("/transform")
     @Operation(summary = "线索转化为客户")
+    @Parameter(name = "id", description = "编号", required = true)
     @PreAuthorize("@ss.hasPermission('crm:clue:update')")
-    public CommonResult<Boolean> translateCustomer(@Valid @RequestBody CrmClueTranslateReqVO reqVO) {
-        clueService.translateCustomer(reqVO, getLoginUserId());
+    public CommonResult<Boolean> transformClue(@RequestParam("id") Long id) {
+        clueService.transformClue(id, getLoginUserId());
         return success(Boolean.TRUE);
     }
 
+    @GetMapping("/follow-count")
+    @Operation(summary = "获得分配给我的、待跟进的线索数量")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<Long> getFollowClueCount() {
+        return success(clueService.getFollowClueCount(getLoginUserId()));
+    }
+
 }

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

@@ -17,6 +17,9 @@ public class CrmCluePageReqVO extends PageParam {
     @Schema(description = "线索名称", example = "线索xxx")
     private String name;
 
+    @Schema(description = "转化状态", example = "2048")
+    private Boolean transformStatus;
+
     @Schema(description = "电话", example = "18000000000")
     private String telephone;
 
@@ -39,4 +42,7 @@ public class CrmCluePageReqVO extends PageParam {
     @Schema(description = "客户来源", example = "1")
     private Integer source;
 
+    @Schema(description = "跟进状态", example = "true")
+    private Boolean followUpStatus;
+
 }

+ 64 - 50
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java

@@ -8,12 +8,9 @@ import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.ToString;
-import org.springframework.format.annotation.DateTimeFormat;
 
 import java.time.LocalDateTime;
 
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-
 @Schema(description = "管理后台 - 线索 Response VO")
 @Data
 @ToString(callSuper = true)
@@ -24,58 +21,76 @@ public class CrmClueRespVO {
     @ExcelProperty("编号")
     private Long id;
 
-    @Schema(description = "转化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
-    @ExcelProperty(value = "转化状态", converter = DictConvert.class)
-    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
-    private Boolean transformStatus;
+    @Schema(description = "线索名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "线索xxx")
+    @ExcelProperty("线索名称")
+    private String name;
 
     @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
     @ExcelProperty(value = "跟进状态", converter = DictConvert.class)
     @DictFormat(DictTypeConstants.BOOLEAN_STRING)
     private Boolean followUpStatus;
 
-    @Schema(description = "线索名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "线索xxx")
-    @ExcelProperty("线索名称")
-    private String name;
+    @Schema(description = "最后跟进时间")
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
 
-    @Schema(description = "客户 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "520")
-    // TODO 这里需要导出成客户名称
-    @ExcelProperty("客户id")
-    private Long customerId;
+    @Schema(description = "最后跟进内容", example = "吃饭、睡觉、打逗逗")
+    @ExcelProperty("最后跟进内容")
+    private String contactLastContent;
 
     @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     @ExcelProperty("下次联系时间")
     private LocalDateTime contactNextTime;
 
-    @Schema(description = "电话", example = "18000000000")
-    @ExcelProperty("电话")
-    private String telephone;
+    @Schema(description = "负责人编号")
+    private Long ownerUserId;
+    @Schema(description = "负责人名字", example = "25682")
+    @ExcelProperty("负责人名字")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    @ExcelProperty("负责人部门")
+    private String ownerUserDeptName;
+
+    @Schema(description = "转化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @ExcelProperty(value = "转化状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean transformStatus;
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "520")
+    private Long customerId;
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户名称")
+    @ExcelProperty("客户名称")
+    private String customerName;
 
     @Schema(description = "手机号", example = "18000000000")
     @ExcelProperty("手机号")
     private String mobile;
 
-    @Schema(description = "地址", example = "北京市海淀区")
-    @ExcelProperty("地址")
-    private String address;
+    @Schema(description = "电话", example = "18000000000")
+    @ExcelProperty("电话")
+    private String telephone;
 
-    @Schema(description = "负责人编号")
-    @ExcelProperty("负责人的用户编号")
-    private Long ownerUserId;
+    @Schema(description = "QQ", example = "25682")
+    @ExcelProperty("QQ")
+    private String qq;
 
-    @Schema(description = "最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    @ExcelProperty("最后跟进时间")
-    private LocalDateTime contactLastTime;
+    @Schema(description = "wechat", example = "25682")
+    @ExcelProperty("wechat")
+    private String wechat;
 
-    @Schema(description = "备注", example = "随便")
-    @ExcelProperty("备注")
-    private String remark;
+    @Schema(description = "email", example = "25682")
+    @ExcelProperty("email")
+    private String email;
 
-    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("创建时间")
-    private LocalDateTime createTime;
+    @Schema(description = "地区编号", example = "1024")
+    @ExcelProperty("地区编号")
+    private Integer areaId;
+    @Schema(description = "地区名称", example = "北京市")
+    @ExcelProperty("地区名称")
+    private String areaName;
+    @Schema(description = "详细地址", example = "北京市成华大道")
+    @ExcelProperty("详细地址")
+    private String detailAddress;
 
     @Schema(description = "所属行业", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     @ExcelProperty(value = "所属行业", converter = DictConvert.class)
@@ -92,24 +107,23 @@ public class CrmClueRespVO {
     @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE)
     private Integer source;
 
-    @Schema(description = "网址", example = "25682")
-    @ExcelProperty("网址")
-    private String website;
-
-    @Schema(description = "QQ", example = "25682")
-    @ExcelProperty("QQ")
-    private String qq;
+    @Schema(description = "备注", example = "随便")
+    @ExcelProperty("备注")
+    private String remark;
 
-    @Schema(description = "wechat", example = "25682")
-    @ExcelProperty("wechat")
-    private String wechat;
+    @Schema(description = "创建人", example = "1024")
+    @ExcelProperty("创建人")
+    private String creator;
+    @Schema(description = "创建人名字", example = "芋道源码")
+    @ExcelProperty("创建人名字")
+    private String creatorName;
 
-    @Schema(description = "email", example = "25682")
-    @ExcelProperty("email")
-    private String email;
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
 
-    @Schema(description = "客户描述", example = "25682")
-    @ExcelProperty("客户描述")
-    private String description;
+    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("更新时间")
+    private LocalDateTime updateTime;
 
 }

+ 41 - 37
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueSaveReqVO.java

@@ -8,10 +8,12 @@ import cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLevelEnum;
 import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerIndustryParseFunction;
 import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerLevelParseFunction;
 import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerSourceParseFunction;
+import cn.iocoder.yudao.module.crm.framework.operatelog.core.SysAreaParseFunction;
 import com.mzt.logapi.starter.annotation.DiffLogField;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.Email;
 import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
@@ -21,7 +23,7 @@ import java.time.LocalDateTime;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY;
 
-@Schema(description = "管理后台 - CRM 线索 创建/更新 Request VO")
+@Schema(description = "管理后台 - CRM 线索创建/更新 Request VO")
 @Data
 public class CrmClueSaveReqVO {
 
@@ -33,36 +35,53 @@ public class CrmClueSaveReqVO {
     @NotEmpty(message = "线索名称不能为空")
     private String name;
 
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @DiffLogField(name = "最后跟进时间")
+    private LocalDateTime contactLastTime;
+
     @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
     @DiffLogField(name = "下次联系时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime contactNextTime;
 
-    @Schema(description = "电话", example = "18000000000")
-    @DiffLogField(name = "电话")
-    @Telephone
-    private String telephone;
+    @Schema(description = "负责人编号", example = "2048")
+    @NotNull(message = "负责人编号不能为空")
+    private Long ownerUserId;
 
     @Schema(description = "手机号", example = "18000000000")
     @DiffLogField(name = "手机号")
     @Mobile
     private String mobile;
 
-    @Schema(description = "地址", example = "北京市海淀区")
-    @DiffLogField(name = "地址")
-    private String address;
+    @Schema(description = "电话", example = "18000000000")
+    @DiffLogField(name = "电话")
+    @Telephone
+    private String telephone;
 
-    @Schema(description = "最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    @DiffLogField(name = "最后跟进时间")
-    private LocalDateTime contactLastTime;
+    @Schema(description = "QQ", example = "123456789")
+    @DiffLogField(name = "QQ")
+    @Size(max = 20, message = "QQ长度不能超过 20 个字符")
+    private String qq;
 
-    @Schema(description = "负责人编号", example = "2048")
-    private Long ownerUserId;
+    @Schema(description = "微信", example = "123456789")
+    @DiffLogField(name = "微信")
+    @Size(max = 255, message = "微信长度不能超过 255 个字符")
+    private String wechat;
 
-    @Schema(description = "备注", example = "随便")
-    @DiffLogField(name = "备注")
-    private String remark;
+    @Schema(description = "邮箱", example = "123456789@qq.com")
+    @DiffLogField(name = "邮箱")
+    @Email(message = "邮箱格式不正确")
+    @Size(max = 255, message = "邮箱长度不能超过 255 个字符")
+    private String email;
+
+    @Schema(description = "地区编号", example = "20158")
+    @DiffLogField(name = "地区编号", function = SysAreaParseFunction.NAME)
+    private Integer areaId;
+
+    @Schema(description = "详细地址", example = "北京市海淀区")
+    @DiffLogField(name = "详细地址")
+    private String detailAddress;
 
     @Schema(description = "所属行业", example = "1")
     @DiffLogField(name = "所属行业", function = CrmCustomerIndustryParseFunction.NAME)
@@ -78,28 +97,13 @@ public class CrmClueSaveReqVO {
     @DiffLogField(name = "客户来源", function = CrmCustomerSourceParseFunction.NAME)
     private Integer source;
 
-    @Schema(description = "网址", example = "https://www.baidu.com")
-    @DiffLogField(name = "网址")
-    private String website;
-
-    @Schema(description = "QQ", example = "123456789")
-    @DiffLogField(name = "QQ")
-    @Size(max = 20, message = "QQ长度不能超过 20 个字符")
-    private String qq;
-
-    @Schema(description = "微信", example = "123456789")
-    @DiffLogField(name = "微信")
-    @Size(max = 255, message = "微信长度不能超过 255 个字符")
-    private String wechat;
-
-    @Schema(description = "邮箱", example = "123456789@qq.com")
-    @DiffLogField(name = "邮箱")
-    @Email(message = "邮箱格式不正确")
-    @Size(max = 255, message = "邮箱长度不能超过 255 个字符")
-    private String email;
-
     @Schema(description = "客户描述", example = "任意文字")
     @DiffLogField(name = "客户描述")
     @Size(max = 4096, message = "客户描述长度不能超过 4096 个字符")
     private String description;
+
+    @Schema(description = "备注", example = "随便")
+    @DiffLogField(name = "备注")
+    private String remark;
+
 }

+ 0 - 17
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueTranslateReqVO.java

@@ -1,17 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotEmpty;
-import lombok.Data;
-
-import java.util.Set;
-
-@Schema(description = "管理后台 - 线索转化为客户 Request VO")
-@Data
-public class CrmClueTranslateReqVO {
-
-    @Schema(description = "线索编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 1025]")
-    @NotEmpty(message = "线索编号不能为空")
-    private Set<Long> ids;
-
-}

+ 73 - 50
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/CrmContactController.java

@@ -2,24 +2,24 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.NumberUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 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.util.ExcelUtils;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
-import cn.iocoder.yudao.module.crm.convert.contact.CrmContactConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
-import cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.crm.service.contact.CrmContactBusinessService;
 import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
-import com.google.common.collect.Lists;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -37,12 +37,13 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static java.util.Collections.singletonList;
 
 @Tag(name = "管理后台 - CRM 联系人")
 @RestController
@@ -60,6 +61,8 @@ public class CrmContactController {
 
     @Resource
     private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建联系人")
@@ -92,36 +95,24 @@ public class CrmContactController {
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<CrmContactRespVO> getContact(@RequestParam("id") Long id) {
         CrmContactDO contact = contactService.getContact(id);
+        return success(buildContactDetail(contact));
+    }
+
+    private CrmContactRespVO buildContactDetail(CrmContactDO contact) {
         if (contact == null) {
-            throw exception(ErrorCodeConstants.CONTACT_NOT_EXISTS);
+            return null;
         }
-        // 1. 获取用户名
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(CollUtil.removeNull(Lists.newArrayList(
-                NumberUtil.parseLong(contact.getCreator()), contact.getOwnerUserId())));
-        // 2. 获取客户信息
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(
-                Collections.singletonList(contact.getCustomerId()));
-        // 3. 直属上级
-        List<CrmContactDO> parentContactList = contactService.getContactListByIds(
-                Collections.singletonList(contact.getParentId()), getLoginUserId());
-        return success(CrmContactConvert.INSTANCE.convert(contact, userMap, customerList, parentContactList));
-    }
-
-    @GetMapping("/list-by-ids")
-    @Operation(summary = "获得联系人列表")
-    @Parameter(name = "ids", description = "编号", required = true, example = "[1024]")
-    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
-    public CommonResult<List<CrmContactRespVO>> getContactListByIds(@RequestParam("ids") List<Long> ids) {
-        return success(BeanUtils.toBean(contactService.getContactListByIds(ids, getLoginUserId()), CrmContactRespVO.class));
+        return buildContactDetailList(singletonList(contact)).get(0);
     }
 
     @GetMapping("/simple-all-list")
     @Operation(summary = "获得联系人的精简列表")
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<List<CrmContactRespVO>> getSimpleContactList() {
-        List<CrmContactDO> list = contactService.getSimpleContactList(getLoginUserId());
+        List<CrmContactDO> list = contactService.getContactList(getLoginUserId());
         return success(convertList(list, contact -> // 只返回 id、name 字段
-                new CrmContactRespVO().setId(contact.getId()).setName(contact.getName())));
+                new CrmContactRespVO().setId(contact.getId()).setName(contact.getName())
+                        .setCustomerId(contact.getCustomerId())));
     }
 
     @GetMapping("/page")
@@ -129,7 +120,7 @@ public class CrmContactController {
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<PageResult<CrmContactRespVO>> getContactPage(@Valid CrmContactPageReqVO pageVO) {
         PageResult<CrmContactDO> pageResult = contactService.getContactPage(pageVO, getLoginUserId());
-        return success(buildContactDetailPage(pageResult));
+        return success(new PageResult<>(buildContactDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/page-by-customer")
@@ -137,7 +128,15 @@ public class CrmContactController {
     public CommonResult<PageResult<CrmContactRespVO>> getContactPageByCustomer(@Valid CrmContactPageReqVO pageVO) {
         Assert.notNull(pageVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmContactDO> pageResult = contactService.getContactPageByCustomerId(pageVO);
-        return success(buildContactDetailPage(pageResult));
+        return success(new PageResult<>(buildContactDetailList(pageResult.getList()), pageResult.getTotal()));
+    }
+
+    @GetMapping("/page-by-business")
+    @Operation(summary = "获得联系人分页,基于指定商机")
+    public CommonResult<PageResult<CrmContactRespVO>> getContactPageByBusiness(@Valid CrmContactPageReqVO pageVO) {
+        Assert.notNull(pageVO.getBusinessId(), "商机编号不能为空");
+        PageResult<CrmContactDO> pageResult = contactService.getContactPageByBusinessId(pageVO);
+        return success(new PageResult<>(buildContactDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/export-excel")
@@ -147,32 +146,39 @@ public class CrmContactController {
     public void exportContactExcel(@Valid CrmContactPageReqVO exportReqVO,
                                    HttpServletResponse response) throws IOException {
         exportReqVO.setPageNo(PAGE_SIZE_NONE);
-        PageResult<CrmContactDO> pageResult = contactService.getContactPage(exportReqVO, getLoginUserId());
-        ExcelUtils.write(response, "联系人.xls", "数据", CrmContactRespVO.class,
-                buildContactDetailPage(pageResult).getList());
-    }
-
-    /**
-     * 构建详细的联系人分页结果
-     *
-     * @param pageResult 简单的联系人分页结果
-     * @return 详细的联系人分页结果
-     */
-    private PageResult<CrmContactRespVO> buildContactDetailPage(PageResult<CrmContactDO> pageResult) {
-        List<CrmContactDO> contactList = pageResult.getList();
+        List<CrmContactDO> list = contactService.getContactPage(exportReqVO, getLoginUserId()).getList();
+        ExcelUtils.write(response, "联系人.xls", "数据", CrmContactRespVO.class, buildContactDetailList(list));
+    }
+
+    private List<CrmContactRespVO> buildContactDetailList(List<CrmContactDO> contactList) {
         if (CollUtil.isEmpty(contactList)) {
-            return PageResult.empty(pageResult.getTotal());
+            return Collections.emptyList();
         }
-        // 1. 获取客户列表
-        List<CrmCustomerDO> crmCustomerDOList = customerService.getCustomerList(
+        // 1.1 获取客户列表
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                 convertSet(contactList, CrmContactDO::getCustomerId));
-        // 2. 获取创建人、负责人列表
+        // 1.2 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(contactList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
-        // 3. 直属上级
-        List<CrmContactDO> parentContactList = contactService.getContactListByIds(
-                convertSet(contactList, CrmContactDO::getParentId), getLoginUserId());
-        return CrmContactConvert.INSTANCE.convertPage(pageResult, userMap, crmCustomerDOList, parentContactList);
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        // 1.3 直属上级 Map
+        Map<Long, CrmContactDO> parentContactMap = contactService.getContactMap(
+                convertSet(contactList, CrmContactDO::getParentId));
+        // 2. 转换成 VO
+        return BeanUtils.toBean(contactList, CrmContactRespVO.class, contactVO -> {
+            contactVO.setAreaName(AreaUtils.format(contactVO.getAreaId()));
+            // 2.1 设置客户名称
+            MapUtils.findAndThen(customerMap, contactVO.getCustomerId(), customer -> contactVO.setCustomerName(customer.getName()));
+            // 2.2 设置创建人、负责人名称
+            MapUtils.findAndThen(userMap, NumberUtils.parseLong(contactVO.getCreator()),
+                    user -> contactVO.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, contactVO.getOwnerUserId(), user -> {
+                contactVO.setOwnerUserName(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> contactVO.setOwnerUserDeptName(dept.getName()));
+            });
+            // 2.3 设置直属上级名称
+            findAndThen(parentContactMap, contactVO.getParentId(), contact -> contactVO.setParentName(contact.getName()));
+        });
     }
 
     @PutMapping("/transfer")
@@ -183,7 +189,7 @@ public class CrmContactController {
         return success(true);
     }
 
-    // ================== 关联/取关联系人  ===================
+    // ================== 关联/取关商机 ===================
 
     @PostMapping("/create-business-list")
     @Operation(summary = "创建联系人与商机的关联")
@@ -193,6 +199,15 @@ public class CrmContactController {
         return success(true);
     }
 
+
+    @PostMapping("/create-business-list2")
+    @Operation(summary = "创建联系人与商机的关联")
+    @PreAuthorize("@ss.hasPermission('crm:contact:create-business')")
+    public CommonResult<Boolean> createContactBusinessList2(@Valid @RequestBody CrmContactBusiness2ReqVO createReqVO) {
+        contactBusinessLinkService.createContactBusinessList2(createReqVO);
+        return success(true);
+    }
+
     @DeleteMapping("/delete-business-list")
     @Operation(summary = "删除联系人与联系人的关联")
     @PreAuthorize("@ss.hasPermission('crm:contact:delete-business')")
@@ -201,4 +216,12 @@ public class CrmContactController {
         return success(true);
     }
 
+    @DeleteMapping("/delete-business-list2")
+    @Operation(summary = "删除联系人与联系人的关联")
+    @PreAuthorize("@ss.hasPermission('crm:contact:delete-business')")
+    public CommonResult<Boolean> deleteContactBusinessList(@Valid @RequestBody CrmContactBusiness2ReqVO deleteReqVO) {
+        contactBusinessLinkService.deleteContactBusinessList2(deleteReqVO);
+        return success(true);
+    }
+
 }

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

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - CRM 联系人商机 Request VO") // 【商机关联联系人】用于关联,取消关联的操作
+@Data
+public class CrmContactBusiness2ReqVO {
+
+    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7638")
+    @NotNull(message="商机不能为空")
+    private Long businessId;
+
+    @Schema(description = "联系人编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "20878")
+    @NotEmpty(message="联系人数组不能为空")
+    private List<Long> contactIds;
+
+}

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactBusinessReqVO.java

@@ -7,7 +7,7 @@ import lombok.Data;
 
 import java.util.List;
 
-@Schema(description = "管理后台 - CRM 联系人商机 Request VO") // 用于关联,取消关联的操作
+@Schema(description = "管理后台 - CRM 联系人商机 Request VO") // 【联系人关联商机】用于关联,取消关联的操作
 @Data
 public class CrmContactBusinessReqVO {
 

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

@@ -39,4 +39,7 @@ public class CrmContactPageReqVO extends PageParam {
     @InEnum(CrmSceneTypeEnum.class)
     private Integer sceneType; // 场景类型,为 null 时则表示全部
 
+    @Schema(description = "商机编号", example = "10430")
+    private Long businessId;
+
 }

+ 55 - 45
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java

@@ -20,29 +20,36 @@ public class CrmContactRespVO {
     @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
     private Long id;
 
-    @Schema(description = "姓名", example = "芋艿")
-    @ExcelProperty(value = "姓名", order = 1)
+    @Schema(description = "联系人姓名", example = "芋艿")
+    @ExcelProperty(value = "联系人姓名", order = 1)
     private String name;
 
     @Schema(description = "客户编号", example = "10795")
     private Long customerId;
+    @ExcelProperty(value = "客户名称", order = 2)
+    @Schema(description = "客户名字", example = "test")
+    private String customerName;
 
-    @Schema(description = "性别")
-    @ExcelProperty(value = "性别", converter = DictConvert.class, order = 3)
-    @DictFormat(cn.iocoder.yudao.module.system.enums.DictTypeConstants.USER_SEX)
-    private Integer sex;
+    @Schema(description = "最后跟进时间")
+    @ExcelProperty(value = "最后跟进时间", order = 6)
+    private LocalDateTime contactLastTime;
 
-    @Schema(description = "职位")
-    @ExcelProperty(value = "职位", order = 3)
-    private String post;
+    @Schema(description = "最后跟进内容")
+    @ExcelProperty(value = "最后跟进内容", order = 6)
+    private String contactLastContent;
 
-    @Schema(description = "是否关键决策人")
-    @ExcelProperty(value = "是否关键决策人", converter = DictConvert.class, order = 3)
-    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
-    private Boolean master;
+    @Schema(description = "下次联系时间")
+    @ExcelProperty(value = "下次联系时间", order = 6)
+    private LocalDateTime contactNextTime;
 
-    @Schema(description = "直属上级", example = "23457")
-    private Long parentId;
+    @Schema(description = "负责人编号")
+    private Long ownerUserId;
+    @Schema(description = "负责人名字", example = "25682")
+    @ExcelProperty("负责人名字")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    @ExcelProperty("负责人部门")
+    private String ownerUserDeptName;
 
     @Schema(description = "手机号", example = "1387171766")
     @ExcelProperty(value = "手机号", order = 4)
@@ -52,6 +59,10 @@ public class CrmContactRespVO {
     @ExcelProperty(value = "电话", order = 4)
     private String telephone;
 
+    @Schema(description = "电子邮箱", example = "1111@22.com")
+    @ExcelProperty(value = "邮箱", order = 4)
+    private String email;
+
     @Schema(description = "QQ", example = "197272662")
     @ExcelProperty(value = "QQ", order = 4)
     private Long qq;
@@ -60,53 +71,52 @@ public class CrmContactRespVO {
     @ExcelProperty(value = "微信", order = 4)
     private String wechat;
 
-    @Schema(description = "电子邮箱", example = "1111@22.com")
-    @ExcelProperty(value = "邮箱", order = 4)
-    private String email;
-
     @Schema(description = "地区编号", example = "20158")
     private Integer areaId;
+    @Schema(description = "地区名", example = "上海上海市浦东新区")
+    @ExcelProperty(value = "地区", order = 5)
+    private String areaName;
 
     @Schema(description = "地址")
     @ExcelProperty(value = "地址", order = 5)
     private String detailAddress;
 
-    @Schema(description = "备注", example = "你说的对")
-    @ExcelProperty(value = "备注", order = 6)
-    private String remark;
+    @Schema(description = "性别")
+    @ExcelProperty(value = "性别", converter = DictConvert.class, order = 3)
+    @DictFormat(cn.iocoder.yudao.module.system.enums.DictTypeConstants.USER_SEX)
+    private Integer sex;
 
-    @Schema(description = "负责人用户编号", example = "14334")
-    private Long ownerUserId;
+    @Schema(description = "是否关键决策人")
+    @ExcelProperty(value = "是否关键决策人", converter = DictConvert.class, order = 3)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean master;
 
-    @Schema(description = "最后跟进时间")
-    @ExcelProperty(value = "最后跟进时间", order = 6)
-    private LocalDateTime contactLastTime;
+    @Schema(description = "职位")
+    @ExcelProperty(value = "职位", order = 3)
+    private String post;
 
-    @Schema(description = "下次联系时间")
-    @ExcelProperty(value = "下次联系时间", order = 6)
-    private LocalDateTime contactNextTime;
+    @Schema(description = "直属上级", example = "23457")
+    private Long parentId;
+    @Schema(description = "直属上级名", example = "芋头")
+    @ExcelProperty(value = "直属上级", order = 4)
+    private String parentName;
+
+    @Schema(description = "备注", example = "你说的对")
+    @ExcelProperty(value = "备注", order = 6)
+    private String remark;
 
     @Schema(description = "创建人", example = "25682")
     private String creator;
-
     @Schema(description = "创建人名字", example = "test")
     @ExcelProperty(value = "创建人", order = 8)
     private String creatorName;
 
-    @ExcelProperty(value = "客户名称", order = 2)
-    @Schema(description = "客户名字", example = "test")
-    private String customerName;
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
 
-    @Schema(description = "负责人", example = "test")
-    @ExcelProperty(value = "负责人", order = 7)
-    private String ownerUserName;
-
-    @Schema(description = "直属上级名", example = "芋头")
-    @ExcelProperty(value = "直属上级", order = 4)
-    private String parentName;
-
-    @Schema(description = "地区名", example = "上海上海市浦东新区")
-    @ExcelProperty(value = "地区", order = 5)
-    private String areaName;
+    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("更新时间")
+    private LocalDateTime updateTime;
 
 }

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

@@ -13,7 +13,6 @@ import org.springframework.format.annotation.DateTimeFormat;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
 @Schema(description = "管理后台 - CRM 联系人创建/更新 Request VO")
 @Data
@@ -28,24 +27,19 @@ public class CrmContactSaveReqVO {
     private String name;
 
     @Schema(description = "客户编号", example = "10795")
+    @NotNull(message = "客户编号不能为空")
     @DiffLogField(name = "客户", function = CrmCustomerParseFunction.NAME)
     private Long customerId;
 
-    @Schema(description = "性别")
-    @DiffLogField(name = "性别", function = SysSexParseFunction.NAME)
-    private Integer sex;
-
-    @Schema(description = "职位")
-    @DiffLogField(name = "职位")
-    private String post;
-
-    @Schema(description = "是否关键决策人")
-    @DiffLogField(name = "关键决策人", function = SysBooleanParseFunction.NAME)
-    private Boolean master;
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+    @DiffLogField(name = "下次联系时间")
+    private LocalDateTime contactNextTime;
 
-    @Schema(description = "直属上级", example = "23457")
-    @DiffLogField(name = "直属上级", function = CrmContactParseFunction.NAME)
-    private Long parentId;
+    @Schema(description = "负责人用户编号", example = "14334")
+    @NotNull(message = "负责人不能为空")
+    @DiffLogField(name = "负责人", function = SysAdminUserParseFunction.NAME)
+    private Long ownerUserId;
 
     @Schema(description = "手机号", example = "1387171766")
     @Mobile
@@ -78,24 +72,25 @@ public class CrmContactSaveReqVO {
     @DiffLogField(name = "地址")
     private String detailAddress;
 
-    @Schema(description = "备注", example = "你说的对")
-    @DiffLogField(name = "备注")
-    private String remark;
+    @Schema(description = "性别")
+    @DiffLogField(name = "性别", function = SysSexParseFunction.NAME)
+    private Integer sex;
 
-    @Schema(description = "负责人用户编号", example = "14334")
-    @NotNull(message = "负责人不能为空")
-    @DiffLogField(name = "负责人", function = SysAdminUserParseFunction.NAME)
-    private Long ownerUserId;
+    @Schema(description = "是否关键决策人")
+    @DiffLogField(name = "关键决策人", function = SysBooleanParseFunction.NAME)
+    private Boolean master;
+
+    @Schema(description = "职位")
+    @DiffLogField(name = "职位")
+    private String post;
 
-    @Schema(description = "最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    @DiffLogField(name = "最后跟进时间")
-    private LocalDateTime contactLastTime;
+    @Schema(description = "直属上级", example = "23457")
+    @DiffLogField(name = "直属上级", function = CrmContactParseFunction.NAME)
+    private Long parentId;
 
-    @Schema(description = "下次联系时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
-    @DiffLogField(name = "下次联系时间")
-    private LocalDateTime contactNextTime;
+    @Schema(description = "备注", example = "你说的对")
+    @DiffLogField(name = "备注")
+    private String remark;
 
     @Schema(description = "关联商机 ID", example = "122233")
     private Long businessId; // 注意:该字段用于在【商机】详情界面「新建联系人」时,自动进行关联

+ 44 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractConfigController.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.config.CrmContractConfigRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.config.CrmContractConfigSaveReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractConfigDO;
+import cn.iocoder.yudao.module.crm.service.contract.CrmContractConfigService;
+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.*;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - CRM 合同配置")
+@RestController
+@RequestMapping("/crm/contract-config")
+@Validated
+public class CrmContractConfigController {
+
+    @Resource
+    private CrmContractConfigService contractConfigService;
+
+    @GetMapping("/get")
+    @Operation(summary = "获取合同配置")
+    @PreAuthorize("@ss.hasPermission('crm:contract-config:query')")
+    public CommonResult<CrmContractConfigRespVO> getCustomerPoolConfig() {
+        CrmContractConfigDO config = contractConfigService.getContractConfig();
+        return success(BeanUtils.toBean(config, CrmContractConfigRespVO.class));
+    }
+
+    @PutMapping("/save")
+    @Operation(summary = "更新合同配置")
+    @PreAuthorize("@ss.hasPermission('crm:contract-config:update')")
+    public CommonResult<Boolean> saveCustomerPoolConfig(@Valid @RequestBody CrmContractConfigSaveReqVO updateReqVO) {
+        contractConfigService.saveContractConfig(updateReqVO);
+        return success(true);
+    }
+
+}

+ 103 - 34
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java

@@ -3,16 +3,17 @@ package cn.iocoder.yudao.module.crm.controller.admin.contract;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 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.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractSaveReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.CrmContractTransferReqVO;
-import cn.iocoder.yudao.module.crm.convert.contract.CrmContractConvert;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractTransferReqVO;
 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;
@@ -24,6 +25,9 @@ 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.customer.CrmCustomerService;
 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.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
@@ -37,6 +41,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.math.BigDecimal;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -44,6 +49,7 @@ import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 import static java.util.Collections.singletonList;
@@ -64,8 +70,13 @@ public class CrmContractController {
     private CrmBusinessService businessService;
     @Resource
     private CrmProductService productService;
+    @Resource
+    private CrmReceivableService receivableService;
+
     @Resource
     private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建合同")
@@ -96,15 +107,24 @@ public class CrmContractController {
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:contract:query')")
     public CommonResult<CrmContractRespVO> getContract(@RequestParam("id") Long id) {
-        // 1. 查询合同
         CrmContractDO contract = contractService.getContract(id);
+        return success(buildContractDetail(contract));
+    }
+
+    private CrmContractRespVO buildContractDetail(CrmContractDO contract) {
         if (contract == null) {
-            return success(null);
+            return null;
         }
-
-        // 2. 拼接合同信息
-        List<CrmContractRespVO> respVOList = buildContractDetailList(singletonList(contract));
-        return success(respVOList.get(0));
+        CrmContractRespVO contractVO = buildContractDetailList(singletonList(contract)).get(0);
+        // 拼接产品项
+        List<CrmContractProductDO> businessProducts = contractService.getContractProductListByContractId(contractVO.getId());
+        Map<Long, CrmProductDO> productMap = productService.getProductMap(
+                convertSet(businessProducts, CrmContractProductDO::getProductId));
+        contractVO.setProducts(BeanUtils.toBean(businessProducts, CrmContractRespVO.Product.class, businessProductVO ->
+                MapUtils.findAndThen(productMap, businessProductVO.getProductId(),
+                        product -> businessProductVO.setProductName(product.getName())
+                                .setProductNo(product.getNo()).setProductUnit(product.getUnit()))));
+        return contractVO;
     }
 
     @GetMapping("/page")
@@ -123,6 +143,14 @@ public class CrmContractController {
         return success(BeanUtils.toBean(pageResult, CrmContractRespVO.class).setList(buildContractDetailList(pageResult.getList())));
     }
 
+    @GetMapping("/page-by-business")
+    @Operation(summary = "获得合同分页,基于指定商机")
+    public CommonResult<PageResult<CrmContractRespVO>> getContractPageByBusiness(@Valid CrmContractPageReqVO pageVO) {
+        Assert.notNull(pageVO.getBusinessId(), "商机编号不能为空");
+        PageResult<CrmContractDO> pageResult = contractService.getContractPageByBusinessId(pageVO);
+        return success(BeanUtils.toBean(pageResult, CrmContractRespVO.class).setList(buildContractDetailList(pageResult.getList())));
+    }
+
     @GetMapping("/export-excel")
     @Operation(summary = "导出合同 Excel")
     @PreAuthorize("@ss.hasPermission('crm:contract:export')")
@@ -151,37 +179,78 @@ public class CrmContractController {
         return success(true);
     }
 
-    /**
-     * 构建详细的合同结果
-     *
-     * @param contractList 原始合同信息
-     * @return 细的合同结果
-     */
     private List<CrmContractRespVO> buildContractDetailList(List<CrmContractDO> contractList) {
         if (CollUtil.isEmpty(contractList)) {
             return Collections.emptyList();
         }
-        // 1. 获取客户列表
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(
+        // 1.1 获取客户列表
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                 convertSet(contractList, CrmContractDO::getCustomerId));
-        // 2. 获取创建人、负责人列表
+        // 1.2 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(contractList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
-        // 3. 获取联系人
-        Map<Long, CrmContactDO> contactMap = convertMap(contactService.getContactListByIds(convertSet(contractList,
-                CrmContractDO::getContactId)), CrmContactDO::getId);
-        // 4. 获取商机
-        Map<Long, CrmBusinessDO> businessMap = convertMap(businessService.getBusinessList(convertSet(contractList,
-                CrmContractDO::getBusinessId)), CrmBusinessDO::getId);
-        // 5. 获取合同关联的商品
-        Map<Long, CrmContractProductDO> contractProductMap = null;
-        List<CrmProductDO> productList = null;
-        if (contractList.size() == 1) {
-            List<CrmContractProductDO> contractProductList = contractService.getContractProductListByContractId(contractList.get(0).getId());
-            contractProductMap = convertMap(contractProductList, CrmContractProductDO::getProductId);
-            productList = productService.getProductListByIds(convertSet(contractProductList, CrmContractProductDO::getProductId));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        // 1.3 获取联系人
+        Map<Long, CrmContactDO> contactMap = convertMap(contactService.getContactList(convertSet(contractList,
+                CrmContractDO::getSignContactId)), CrmContactDO::getId);
+        // 1.4 获取商机
+        Map<Long, CrmBusinessDO> businessMap = businessService.getBusinessMap(
+                convertSet(contractList, CrmContractDO::getBusinessId));
+        // 1.5 获得已回款金额
+        Map<Long, BigDecimal> receivablePriceMap = receivableService.getReceivablePriceMapByContractId(
+                convertSet(contractList, CrmContractDO::getId));
+        // 2. 拼接数据
+        return BeanUtils.toBean(contractList, CrmContractRespVO.class, contractVO -> {
+            // 2.1 设置客户信息
+            findAndThen(customerMap, contractVO.getCustomerId(), customer -> contractVO.setCustomerName(customer.getName()));
+            // 2.2 设置用户信息
+            findAndThen(userMap, Long.parseLong(contractVO.getCreator()), user -> contractVO.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, contractVO.getOwnerUserId(), user -> {
+                contractVO.setOwnerUserName(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> contractVO.setOwnerUserDeptName(dept.getName()));
+            });
+            findAndThen(userMap, contractVO.getSignUserId(), user -> contractVO.setSignUserName(user.getNickname()));
+            // 2.3 设置联系人信息
+            findAndThen(contactMap, contractVO.getSignContactId(), contact -> contractVO.setSignContactName(contact.getName()));
+            // 2.4 设置商机信息
+            findAndThen(businessMap, contractVO.getBusinessId(), business -> contractVO.setBusinessName(business.getName()));
+            // 2.5 设置已回款金额
+            contractVO.setTotalReceivablePrice(receivablePriceMap.getOrDefault(contractVO.getId(), BigDecimal.ZERO));
+        });
+    }
+
+    @GetMapping("/audit-count")
+    @Operation(summary = "获得待审核合同数量")
+    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
+    public CommonResult<Long> getAuditContractCount() {
+        return success(contractService.getAuditContractCount(getLoginUserId()));
+    }
+
+    @GetMapping("/remind-count")
+    @Operation(summary = "获得即将到期(提醒)的合同数量")
+    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
+    public CommonResult<Long> getRemindContractCount() {
+        return success(contractService.getRemindContractCount(getLoginUserId()));
+    }
+
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得合同精简列表", description = "只包含的合同,主要用于前端的下拉选项")
+    @Parameter(name = "customerId", description = "客户编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
+    public CommonResult<List<CrmContractRespVO>> getContractSimpleList(@RequestParam("customerId") Long customerId) {
+        CrmContractPageReqVO pageReqVO = new CrmContractPageReqVO().setCustomerId(customerId);
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); // 不分页
+        PageResult<CrmContractDO> pageResult = contractService.getContractPageByCustomerId(pageReqVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(Collections.emptyList());
         }
-        return CrmContractConvert.INSTANCE.convertList(contractList, userMap, customerList, contactMap, businessMap, contractProductMap, productList);
+        // 拼接数据
+        Map<Long, BigDecimal> receivablePriceMap = receivableService.getReceivablePriceMapByContractId(
+                convertSet(pageResult.getList(), CrmContractDO::getId));
+        return success(convertList(pageResult.getList(), contract -> new CrmContractRespVO() // 只返回 id、name 等精简字段
+                .setId(contract.getId()).setName(contract.getName()).setAuditStatus(contract.getAuditStatus())
+                .setTotalPrice(contract.getTotalPrice())
+                .setTotalReceivablePrice(receivablePriceMap.getOrDefault(contract.getId(), BigDecimal.ZERO))));
     }
 
 }

+ 16 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/config/CrmContractConfigRespVO.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.config;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 合同配置 Response VO")
+@Data
+public class CrmContractConfigRespVO {
+
+    @Schema(description = "是否开启提前提醒", example = "true")
+    private Boolean notifyEnabled;
+
+    @Schema(description = "提前提醒天数", example = "2")
+    private Integer notifyDays;
+
+}

+ 33 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/config/CrmContractConfigSaveReqVO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.config;
+
+import cn.hutool.core.util.BooleanUtil;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.mzt.logapi.starter.annotation.DiffLogField;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
+import lombok.Data;
+
+import java.util.Objects;
+
+@Schema(description = "管理后台 - CRM 合同配置 Request VO")
+@Data
+public class CrmContractConfigSaveReqVO {
+
+    @Schema(description = "是否开启提前提醒", example = "true")
+    @DiffLogField(name = "是否开启提前提醒")
+    private Boolean notifyEnabled;
+
+    @Schema(description = "提前提醒天数", example = "2")
+    @DiffLogField(name = "提前提醒天数")
+    private Integer notifyDays;
+
+    @AssertTrue(message = "提前提醒天数不能为空")
+    @JsonIgnore
+    public boolean isNotifyDaysValid() {
+        if (!BooleanUtil.isTrue(getNotifyEnabled())) {
+            return true;
+        }
+        return Objects.nonNull(getNotifyDays());
+    }
+
+}

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractPageReqVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractPageReqVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.validation.InEnum;

+ 61 - 64
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractRespVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract;
 
 import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import com.alibaba.excel.annotation.ExcelProperty;
@@ -6,13 +6,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.springframework.format.annotation.DateTimeFormat;
 
+import java.math.BigDecimal;
 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 合同 Response VO")
 @Data
 @ExcelIgnoreUnannotated
@@ -26,6 +24,10 @@ public class CrmContractRespVO {
     @ExcelProperty("合同名称")
     private String name;
 
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20230101")
+    @ExcelProperty("合同编号")
+    private String no;
+
     @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18336")
     @ExcelProperty("客户编号")
     private Long customerId;
@@ -40,72 +42,74 @@ public class CrmContractRespVO {
     @ExcelProperty("商机名称")
     private String businessName;
 
+    @Schema(description = "最后跟进时间")
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "负责人的用户编号", example = "25682")
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+    @Schema(description = "负责人名字", example = "25682")
+    @ExcelProperty("负责人名字")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    @ExcelProperty("负责人部门")
+    private String ownerUserDeptName;
+
     @Schema(description = "工作流编号", example = "1043")
     @ExcelProperty("工作流编号")
-    private Long processInstanceId;
+    private String processInstanceId;
+
+    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    @ExcelProperty("审批状态")
+    private Integer auditStatus;
 
     @Schema(description = "下单日期", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("下单日期")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime orderDate;
 
-    @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17144")
-    @ExcelProperty("负责人的用户编号")
-    private Long ownerUserId;
-
-    // TODO @芋艿:未来应该支持自动生成;
-    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20230101")
-    @ExcelProperty("合同编号")
-    private String no;
-
     @Schema(description = "开始时间")
     @ExcelProperty("开始时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime startTime;
 
     @Schema(description = "结束时间")
     @ExcelProperty("结束时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime endTime;
 
-    @Schema(description = "合同金额", example = "5617")
-    @ExcelProperty("合同金额")
-    private Integer price;
+    @Schema(description = "产品总金额", example = "19510")
+    @ExcelProperty("产品总金额")
+    private BigDecimal totalProductPrice;
 
     @Schema(description = "整单折扣")
     @ExcelProperty("整单折扣")
-    private Integer discountPercent;
+    private BigDecimal discountPercent;
 
-    @Schema(description = "产品总金额", example = "19510")
-    @ExcelProperty("产品总金额")
-    private Integer productPrice;
+    @Schema(description = "合同金额", example = "5617")
+    @ExcelProperty("合同金额")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "已回款金额", example = "5617")
+    @ExcelProperty("已回款金额")
+    private BigDecimal totalReceivablePrice;
 
-    @Schema(description = "联系人编号", example = "18546")
-    @ExcelProperty("联系人编号")
-    private Long contactId;
-    @Schema(description = "联系人编号", example = "18546")
-    @ExcelProperty("联系人编号")
-    private String contactName;
+    @Schema(description = "客户签约人编号", example = "18546")
+    private Long signContactId;
+    @Schema(description = "客户签约人", example = "小豆")
+    @ExcelProperty("客户签约人")
+    private String signContactName;
 
     @Schema(description = "公司签约人", example = "14036")
-    @ExcelProperty("公司签约人")
     private Long signUserId;
-    @Schema(description = "公司签约人", example = "14036")
+    @Schema(description = "公司签约人", example = "小明")
     @ExcelProperty("公司签约人")
     private String signUserName;
 
-    @Schema(description = "最后跟进时间")
-    @ExcelProperty("最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime contactLastTime;
-
     @Schema(description = "备注", example = "你猜")
     @ExcelProperty("备注")
     private String remark;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("创建时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime createTime;
 
     @Schema(description = "创建人", example = "25682")
@@ -118,47 +122,40 @@ public class CrmContractRespVO {
 
     @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("更新时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime updateTime;
 
-    @Schema(description = "负责人", example = "test")
-    @ExcelProperty("负责人")
-    private String ownerUserName;
-
-    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
-    @ExcelProperty("审批状态")
-    private Integer auditStatus;
-
     @Schema(description = "产品列表")
-    private List<CrmContractProductItemRespVO> productItems;
+    private List<Product> products;
 
-    // TODO @puhui999:可以直接叫 Item
     @Schema(description = "产品列表")
     @Data
     @NoArgsConstructor
     @AllArgsConstructor
-    public static class CrmContractProductItemRespVO {
+    public static class Product {
 
-        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+        @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888")
         private Long id;
 
-        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是产品")
-        private String name;
-
-        @Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "N881")
-        private String no;
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+        private Long productId;
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
+        private String productNo;
+        @Schema(description = "产品单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+        private Integer productUnit;
 
-        @Schema(description = "单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
-        private Integer unit;
+        @Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        private BigDecimal productPrice;
 
-        @Schema(description = "价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
-        private Integer price;
+        @Schema(description = "合同价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        private BigDecimal contractPrice;
 
-        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
-        private Integer count;
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
+        private BigDecimal count;
 
-        @Schema(description = "产品折扣", example = "99")
-        private Integer discountPercent;
+        @Schema(description = "总计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        private BigDecimal totalPrice;
 
     }
 

+ 28 - 32
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractSaveReqVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract;
 
 import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmBusinessParseFunction;
 import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmContactParseFunction;
@@ -12,6 +12,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.springframework.format.annotation.DateTimeFormat;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -38,22 +39,16 @@ public class CrmContractSaveReqVO {
     @DiffLogField(name = "商机", function = CrmBusinessParseFunction.NAME)
     private Long businessId;
 
-    @Schema(description = "下单日期", requiredMode = Schema.RequiredMode.REQUIRED)
-    @DiffLogField(name = "下单日期")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    @NotNull(message = "下单日期不能为空")
-    private LocalDateTime orderDate;
-
     @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17144")
     @DiffLogField(name = "负责人", function = SysAdminUserParseFunction.NAME)
     @NotNull(message = "负责人不能为空")
     private Long ownerUserId;
 
-    // TODO @芋艿:未来应该支持自动生成;
-    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20230101")
-    @DiffLogField(name = "合同编号")
-    @NotNull(message = "合同编号不能为空")
-    private String no;
+    @Schema(description = "下单日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @DiffLogField(name = "下单日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @NotNull(message = "下单日期不能为空")
+    private LocalDateTime orderDate;
 
     @Schema(description = "开始时间")
     @DiffLogField(name = "开始时间")
@@ -65,21 +60,18 @@ public class CrmContractSaveReqVO {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime endTime;
 
-    @Schema(description = "合同金额", example = "5617")
-    @DiffLogField(name = "合同金额")
-    private Integer price;
-
-    @Schema(description = "整单折扣")
+    @Schema(description = "整单折扣", requiredMode = Schema.RequiredMode.REQUIRED, example = "55.00")
     @DiffLogField(name = "整单折扣")
-    private Integer discountPercent;
+    @NotNull(message = "整单折扣不能为空")
+    private BigDecimal discountPercent;
 
-    @Schema(description = "产品总金额", example = "19510")
-    @DiffLogField(name = "产品总金额")
-    private Integer productPrice;
+    @Schema(description = "合同金额", example = "5617")
+    @DiffLogField(name = "合同金额")
+    private BigDecimal totalPrice;
 
-    @Schema(description = "联系人编号", example = "18546")
-    @DiffLogField(name = "联系人", function = CrmContactParseFunction.NAME)
-    private Long contactId;
+    @Schema(description = "客户签约人编号", example = "18546")
+    @DiffLogField(name = "客户签约人", function = CrmContactParseFunction.NAME)
+    private Long signContactId;
 
     @Schema(description = "公司签约人", example = "14036")
     @DiffLogField(name = "公司签约人", function = SysAdminUserParseFunction.NAME)
@@ -89,27 +81,31 @@ public class CrmContractSaveReqVO {
     @DiffLogField(name = "备注")
     private String remark;
 
-
     @Schema(description = "产品列表")
-    private List<CrmContractProductItem> productItems;
+    private List<Product> products;
 
     @Schema(description = "产品列表")
     @Data
     @NoArgsConstructor
     @AllArgsConstructor
-    public static class CrmContractProductItem {
+    public static class Product {
 
-        @Schema(description = "产品编号", example = "20529")
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
         @NotNull(message = "产品编号不能为空")
-        private Long id;
+        private Long productId;
+
+        @Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        @NotNull(message = "产品单价不能为空")
+        private BigDecimal productPrice;
+
+        @Schema(description = "合同价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
+        @NotNull(message = "合同价格不能为空")
+        private BigDecimal contractPrice;
 
         @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
         @NotNull(message = "产品数量不能为空")
         private Integer count;
 
-        @Schema(description = "产品折扣")
-        private Integer discountPercent;
-
     }
 
 }

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

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+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;

+ 0 - 16
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http

@@ -1,16 +0,0 @@
-### 请求 /transfer
-PUT {{baseUrl}}/crm/customer/transfer
-Content-Type: application/-id: {{adminTenentId}}json
-Authorization: Bearer {{token}}
-tenant
-
-{
-  "id": 10,
-  "newOwnerUserId": 127
-}
-
-### 自定义日志记录结果
-### 操作日志 ===> OperateLogV2CreateReqBO(traceId=, userId=1, userType=2, module=CRM-客户, name=客户转移, bizId=10, content=把客户【张三】的负责人从【芋道源码(15612345678)】变更为了【tttt】, requestMethod=PUT, requestUrl=/admin-api/crm/customer/transfer, userIp=127.0.0.1, userAgent=Apache-HttpClient/4.5.14 (Java/17.0.9))
-
-### diff 日志
-### | 操作日志 ===> OperateLogV2CreateReqBO(traceId=, userId=1, userType=2, module=CRM-客户, name=更新客户, bizId=11, content=更新了客户【所属行业】从【H 住宿和餐饮业】修改为【D 电力、热力、燃气及水生产和供应业】;【客户等级】从【C (非优先客户)】修改为【A (重点客户)】;【客户来源】从【线上咨询】修改为【预约上门】, requestMethod=PUT, requestUrl=/admin-api/crm/customer/update, userIp=0:0:0:0:0:0:0:1, userAgent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36)

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

@@ -2,48 +2,55 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
-import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.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.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
-import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerPoolConfigService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.Valid;
-import org.mapstruct.ap.internal.util.Collections;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED;
+import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.*;
+import static java.util.Collections.singletonList;
 
 @Tag(name = "管理后台 - CRM 客户")
 @RestController
@@ -55,10 +62,13 @@ public class CrmCustomerController {
     private CrmCustomerService customerService;
     @Resource
     private CrmCustomerPoolConfigService customerPoolConfigService;
+
     @Resource
     private DeptApi deptApi;
     @Resource
     private AdminUserApi adminUserApi;
+    @Resource
+    private DictDataApi dictDataApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建客户")
@@ -75,6 +85,18 @@ public class CrmCustomerController {
         return success(true);
     }
 
+    @PutMapping("/update-deal-status")
+    @Operation(summary = "更新客户的成交状态")
+    @Parameters({
+            @Parameter(name = "id", description = "客户编号", required = true),
+            @Parameter(name = "dealStatus", description = "成交状态", required = true)
+    })
+    public CommonResult<Boolean> updateCustomerDealStatus(@RequestParam("id") Long id,
+                                                          @RequestParam("dealStatus") Boolean dealStatus) {
+        customerService.updateCustomerDealStatus(id, dealStatus);
+        return success(true);
+    }
+
     @DeleteMapping("/delete")
     @Operation(summary = "删除客户")
     @Parameter(name = "id", description = "客户编号", required = true)
@@ -91,94 +113,126 @@ public class CrmCustomerController {
     public CommonResult<CrmCustomerRespVO> getCustomer(@RequestParam("id") Long id) {
         // 1. 获取客户
         CrmCustomerDO customer = customerService.getCustomer(id);
+        // 2. 拼接数据
+        return success(buildCustomerDetail(customer));
+    }
+
+    public CrmCustomerRespVO buildCustomerDetail(CrmCustomerDO customer) {
         if (customer == null) {
-            return success(null);
+            return null;
         }
-        // 2. 拼接数据
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                Collections.asSet(Long.valueOf(customer.getCreator()), customer.getOwnerUserId()));
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
-        return success(CrmCustomerConvert.INSTANCE.convert(customer, userMap, deptMap));
+        return buildCustomerDetailList(singletonList(customer)).get(0);
     }
 
     @GetMapping("/page")
     @Operation(summary = "获得客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
     public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
+        customerService.autoPutCustomerPool();
         // 1. 查询客户分页
         PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO, getLoginUserId());
         if (CollUtil.isEmpty(pageResult.getList())) {
             return success(PageResult.empty(pageResult.getTotal()));
         }
-
         // 2. 拼接数据
-        Map<Long, Long> poolDayMap = Boolean.TRUE.equals(pageVO.getPool()) ? null :
-                getPoolDayMap(pageResult.getList()); // 客户界面,需要查看距离进入公海的时间
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                convertSetByFlatMap(pageResult.getList(), user -> Stream.of(Long.parseLong(user.getCreator()), user.getOwnerUserId())));
+        return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal()));
+    }
+
+    public List<CrmCustomerRespVO> buildCustomerDetailList(List<CrmCustomerDO> list) {
+        if (CollUtil.isEmpty(list)) {
+            return java.util.Collections.emptyList();
+        }
+        // 1.1 获取创建人、负责人列表
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(list,
+                contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
-        return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap, poolDayMap));
+        // 1.2 获取距离进入公海的时间
+        Map<Long, Long> poolDayMap = getPoolDayMap(list);
+        // 2. 转换成 VO
+        return BeanUtils.toBean(list, CrmCustomerRespVO.class, customerVO -> {
+            customerVO.setAreaName(AreaUtils.format(customerVO.getAreaId()));
+            // 2.1 设置创建人、负责人名称
+            MapUtils.findAndThen(userMap, NumberUtils.parseLong(customerVO.getCreator()),
+                    user -> customerVO.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, customerVO.getOwnerUserId(), user -> {
+                customerVO.setOwnerUserName(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> customerVO.setOwnerUserDeptName(dept.getName()));
+            });
+            // 2.2 设置距离进入公海的时间
+            if (customerVO.getOwnerUserId() != null) {
+                customerVO.setPoolDay(poolDayMap.get(customerVO.getId()));
+            }
+        });
     }
 
-    @GetMapping("/put-in-pool-remind-page")
+    @GetMapping("/put-pool-remind-page")
     @Operation(summary = "获得待进入公海客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
-    public CommonResult<PageResult<CrmCustomerRespVO>> getPutInPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
-        // 获取公海配置 TODO @dbh52:合并到 getPutInPoolRemindCustomerPage 会更合适哈;
-        CrmCustomerPoolConfigDO poolConfigDO = customerPoolConfigService.getCustomerPoolConfig();
-        if (ObjUtil.isNull(poolConfigDO)
-                || Boolean.FALSE.equals(poolConfigDO.getEnabled())
-                || Boolean.FALSE.equals(poolConfigDO.getNotifyEnabled())
-        ) { // TODO @dbh52:这个括号,一般不换行,在 java 这里;
-            throw exception(CUSTOMER_POOL_CONFIG_NOT_EXISTS_OR_DISABLED);
-        }
-
+    public CommonResult<PageResult<CrmCustomerRespVO>> getPutPoolRemindCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
         // 1. 查询客户分页
-        PageResult<CrmCustomerDO> pageResult = customerService.getPutInPoolRemindCustomerPage(pageVO, poolConfigDO, getLoginUserId());
-        if (CollUtil.isEmpty(pageResult.getList())) {
-            return success(PageResult.empty(pageResult.getTotal()));
-        }
-
+        PageResult<CrmCustomerDO> pageResult = customerService.getPutPoolRemindCustomerPage(pageVO, getLoginUserId());
         // 2. 拼接数据
-        // TODO @芋艿:合并 getCustomerPage 和 getPutInPoolRemindCustomerPage 的后置处理;
-        Map<Long, Long> poolDayMap = getPoolDayMap(pageResult.getList()); // 客户界面,需要查看距离进入公海的时间
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                convertSetByFlatMap(pageResult.getList(), user -> Stream.of(Long.parseLong(user.getCreator()), user.getOwnerUserId())));
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
-        return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, userMap, deptMap, poolDayMap));
+        return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal()));
+    }
+
+    @GetMapping("/put-pool-remind-count")
+    @Operation(summary = "获得待进入公海客户数量")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<Long> getPutPoolRemindCustomerCount() {
+        return success(customerService.getPutPoolRemindCustomerCount(getLoginUserId()));
+    }
+
+    @GetMapping("/today-contact-count")
+    @Operation(summary = "获得今日需联系客户数量")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<Long> getTodayContactCustomerCount() {
+        return success(customerService.getTodayContactCustomerCount(getLoginUserId()));
+    }
+
+    @GetMapping("/follow-count")
+    @Operation(summary = "获得分配给我、待跟进的线索数量的客户数量")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<Long> getFollowCustomerCount() {
+        return success(customerService.getFollowCustomerCount(getLoginUserId()));
     }
 
     /**
-     * 获取距离进入公海的时间
+     * 获取距离进入公海的时间 Map
      *
-     * @param customerList 客户列表
-     * @return Map<key 客户编号, value 距离进入公海的时间>
+     * @param list 客户列表
+     * @return key 客户编号, value 距离进入公海的时间
      */
-    private Map<Long, Long> getPoolDayMap(List<CrmCustomerDO> customerList) {
+    private Map<Long, Long> getPoolDayMap(List<CrmCustomerDO> list) {
         CrmCustomerPoolConfigDO poolConfig = customerPoolConfigService.getCustomerPoolConfig();
         if (poolConfig == null || !poolConfig.getEnabled()) {
             return MapUtil.empty();
         }
-        return convertMap(customerList, CrmCustomerDO::getId, customer -> {
-            // 1.1 未成交放入公海天数
-            long dealExpireDay = 0;
-            if (!customer.getDealStatus()) {
-                dealExpireDay = poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getCreateTime());
+        list = CollectionUtils.filterList(list, customer -> {
+            // 特殊:如果没负责人,则说明已经在公海,不用计算
+            if (customer.getOwnerUserId() == null) {
+                return false;
             }
+            // 已成交 or 已锁定,不进入公海
+            return !customer.getDealStatus() && !customer.getLockStatus();
+        });
+        return convertMap(list, CrmCustomerDO::getId, customer -> {
+            // 1.1 未成交放入公海天数
+            long dealExpireDay = poolConfig.getDealExpireDays() - LocalDateTimeUtils.between(customer.getOwnerTime());
             // 1.2 未跟进放入公海天数
-            LocalDateTime lastTime = ObjUtil.defaultIfNull(customer.getContactLastTime(), customer.getCreateTime());
-            long contactExpireDay = poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime);
-            if (contactExpireDay < 0) {
-                contactExpireDay = 0;
+            LocalDateTime lastTime = customer.getOwnerTime();
+            if (customer.getContactLastTime() != null && customer.getContactLastTime().isAfter(lastTime)) {
+                lastTime = customer.getContactLastTime();
             }
+            long contactExpireDay = poolConfig.getContactExpireDays() - LocalDateTimeUtils.between(lastTime);
             // 2. 返回最小的天数
-            return Math.min(dealExpireDay, contactExpireDay);
+            long poolDay = Math.min(dealExpireDay, contactExpireDay);
+            return poolDay > 0 ? poolDay : 0;
         });
     }
 
-    @GetMapping(value = "/list-all-simple")
+    @GetMapping(value = "/simple-list")
     @Operation(summary = "获取客户精简信息列表", description = "只包含有读权限的客户,主要用于前端的下拉选项")
-    public CommonResult<List<CrmCustomerRespVO>> getSimpleDeptList() {
+    public CommonResult<List<CrmCustomerRespVO>> getCustomerSimpleList() {
         CrmCustomerPageReqVO reqVO = new CrmCustomerPageReqVO();
         reqVO.setPageSize(PAGE_SIZE_NONE); // 不分页
         List<CrmCustomerDO> list = customerService.getCustomerPage(reqVO, getLoginUserId()).getList();
@@ -186,7 +240,6 @@ public class CrmCustomerController {
                 new CrmCustomerRespVO().setId(customer.getId()).setName(customer.getName())));
     }
 
-    // TODO @puhui999:公海的导出,前端可以接下
     @GetMapping("/export-excel")
     @Operation(summary = "导出客户 Excel")
     @PreAuthorize("@ss.hasPermission('crm:customer:export')")
@@ -197,7 +250,7 @@ public class CrmCustomerController {
         List<CrmCustomerDO> list = customerService.getCustomerPage(pageVO, getLoginUserId()).getList();
         // 导出 Excel
         ExcelUtils.write(response, "客户.xls", "数据", CrmCustomerRespVO.class,
-                BeanUtils.toBean(list, CrmCustomerRespVO.class));
+                buildCustomerDetailList(list));
     }
 
     @GetMapping("/get-import-template")
@@ -205,15 +258,33 @@ public class CrmCustomerController {
     public void importTemplate(HttpServletResponse response) throws IOException {
         // 手动创建导出 demo
         List<CrmCustomerImportExcelVO> list = Arrays.asList(
-                CrmCustomerImportExcelVO.builder().name("芋道").industryId(1).level(1).source(1).mobile("15601691300").telephone("")
-                        .website("https://doc.iocoder.cn/").qq("").wechat("").email("yunai@iocoder.cn").description("").remark("")
-                        .areaId(null).detailAddress("").build(),
-                CrmCustomerImportExcelVO.builder().name("源码").industryId(1).level(1).source(1).mobile("15601691300").telephone("")
-                        .website("https://doc.iocoder.cn/").qq("").wechat("").email("yunai@iocoder.cn").description("").remark("")
-                        .areaId(null).detailAddress("").build()
+                CrmCustomerImportExcelVO.builder().name("芋道").industryId(1).level(1).source(1)
+                        .mobile("15601691300").telephone("").qq("").wechat("").email("yunai@iocoder.cn")
+                        .areaId(null).detailAddress("").remark("").build(),
+                CrmCustomerImportExcelVO.builder().name("源码").industryId(1).level(1).source(1)
+                        .mobile("15601691300").telephone("").qq("").wechat("").email("yunai@iocoder.cn")
+                        .areaId(null).detailAddress("").remark("").build()
         );
         // 输出
-        ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list);
+        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.parseArea(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;
     }
 
     @PostMapping("/import")

+ 13 - 6
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java

@@ -3,10 +3,11 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerLimitConfigConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerLimitConfigService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -71,11 +72,14 @@ public class CrmCustomerLimitConfigController {
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:query')")
     public CommonResult<CrmCustomerLimitConfigRespVO> getCustomerLimitConfig(@RequestParam("id") Long id) {
-        CrmCustomerLimitConfigDO customerLimitConfig = customerLimitConfigService.getCustomerLimitConfig(id);
+        CrmCustomerLimitConfigDO limitConfig = customerLimitConfigService.getCustomerLimitConfig(id);
         // 拼接数据
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(customerLimitConfig.getUserIds());
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(customerLimitConfig.getDeptIds());
-        return success(CrmCustomerLimitConfigConvert.INSTANCE.convert(customerLimitConfig, userMap, deptMap));
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(limitConfig.getUserIds());
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(limitConfig.getDeptIds());
+        return success(BeanUtils.toBean(limitConfig, CrmCustomerLimitConfigRespVO.class, configVO -> {
+            configVO.setUsers(CollectionUtils.convertList(configVO.getUserIds(), userMap::get));
+            configVO.setDepts(CollectionUtils.convertList(configVO.getDeptIds(), deptMap::get));
+        }));
     }
 
     @GetMapping("/page")
@@ -91,7 +95,10 @@ public class CrmCustomerLimitConfigController {
                 convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getUserIds, Collection::stream));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
                 convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getDeptIds, Collection::stream));
-        return success(CrmCustomerLimitConfigConvert.INSTANCE.convertPage(pageResult, userMap, deptMap));
+        return success(BeanUtils.toBean(pageResult, CrmCustomerLimitConfigRespVO.class, configVO -> {
+            configVO.setUsers(CollectionUtils.convertList(configVO.getUserIds(), userMap::get));
+            configVO.setDepts(CollectionUtils.convertList(configVO.getDeptIds(), deptMap::get));
+        }));
     }
 
 }

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

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotEmpty;

+ 18 - 25
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerImportExcelVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java

@@ -1,6 +1,7 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+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.convert.AreaConvert;
 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 import com.alibaba.excel.annotation.ExcelProperty;
 import lombok.AllArgsConstructor;
@@ -24,28 +25,12 @@ public class CrmCustomerImportExcelVO {
     @ExcelProperty("客户名称")
     private String name;
 
-    // TODO @puhui999:industryId、level、source 字段,可以研究下怎么搞下拉框
-    @ExcelProperty(value = "所属行业", converter = DictConvert.class)
-    @DictFormat(CRM_CUSTOMER_INDUSTRY)
-    private Integer industryId;
-
-    @ExcelProperty(value = "客户等级", converter = DictConvert.class)
-    @DictFormat(CRM_CUSTOMER_LEVEL)
-    private Integer level;
-
-    @ExcelProperty(value = "客户来源", converter = DictConvert.class)
-    @DictFormat(CRM_CUSTOMER_SOURCE)
-    private Integer source;
-
     @ExcelProperty("手机")
     private String mobile;
 
     @ExcelProperty("电话")
     private String telephone;
 
-    @ExcelProperty("网址")
-    private String website;
-
     @ExcelProperty("QQ")
     private String qq;
 
@@ -55,17 +40,25 @@ public class CrmCustomerImportExcelVO {
     @ExcelProperty("邮箱")
     private String email;
 
-    @ExcelProperty("客户描述")
-    private String description;
-
-    @ExcelProperty("备注")
-    private String remark;
-
-    // TODO @puhui999:需要选择省市区,需要研究下,怎么搞合理点;
-    @ExcelProperty("地区编号")
+    @ExcelProperty(value = "地区", converter = AreaConvert.class)
     private Integer areaId;
 
     @ExcelProperty("详细地址")
     private String detailAddress;
 
+    @ExcelProperty(value = "所属行业", converter = DictConvert.class)
+    @DictFormat(CRM_CUSTOMER_INDUSTRY)
+    private Integer industryId;
+
+    @ExcelProperty(value = "客户等级", converter = DictConvert.class)
+    @DictFormat(CRM_CUSTOMER_LEVEL)
+    private Integer level;
+
+    @ExcelProperty(value = "客户来源", converter = DictConvert.class)
+    @DictFormat(CRM_CUSTOMER_SOURCE)
+    private Integer source;
+
+    @ExcelProperty("备注")
+    private String remark;
+
 }

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

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;

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

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Builder;

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

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;

+ 20 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerPageReqVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerPageReqVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.validation.InEnum;
@@ -14,6 +14,19 @@ import lombok.ToString;
 @ToString(callSuper = true)
 public class CrmCustomerPageReqVO extends PageParam {
 
+    /**
+     * 联系状态 - 今日需联系
+     */
+    public static final int CONTACT_TODAY = 1;
+    /**
+     * 联系状态 - 已逾期
+     */
+    public static final int CONTACT_EXPIRED = 2;
+    /**
+     * 联系状态 - 已联系
+     */
+    public static final int CONTACT_ALREADY = 3;
+
     @Schema(description = "客户名称", example = "赵六")
     private String name;
 
@@ -36,4 +49,10 @@ public class CrmCustomerPageReqVO extends PageParam {
     @Schema(description = "是否为公海数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
     private Boolean pool; // null 则表示为不是公海数据
 
+    @Schema(description = "联系状态", example = "1")
+    private Integer contactStatus; // backlog 查询条件
+
+    @Schema(description = "跟进状态", example = "true")
+    private Boolean followUpStatus;
+
 }

+ 45 - 53
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerRespVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+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.convert.DictConvert;
@@ -7,12 +7,9 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
-import org.springframework.format.annotation.DateTimeFormat;
 
 import java.time.LocalDateTime;
 
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-
 @Schema(description = "管理后台 - CRM 客户 Response VO")
 @Data
 @ExcelIgnoreUnannotated
@@ -31,6 +28,28 @@ public class CrmCustomerRespVO {
     @DictFormat(DictTypeConstants.BOOLEAN_STRING)
     private Boolean followUpStatus;
 
+    @Schema(description = "最后跟进时间")
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "最后跟进内容", example = "吃饭、睡觉、打逗逗")
+    @ExcelProperty("最后跟进内容")
+    private String contactLastContent;
+
+    @Schema(description = "下次联系时间")
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @Schema(description = "负责人的用户编号", example = "25682")
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+    @Schema(description = "负责人名字", example = "25682")
+    @ExcelProperty("负责人名字")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    @ExcelProperty("负责人部门")
+    private String ownerUserDeptName;
+
     @Schema(description = "锁定状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     @ExcelProperty(value = "锁定状态", converter = DictConvert.class)
     @DictFormat(DictTypeConstants.BOOLEAN_STRING)
@@ -41,63 +60,26 @@ public class CrmCustomerRespVO {
     @DictFormat(DictTypeConstants.BOOLEAN_STRING)
     private Boolean dealStatus;
 
-    @Schema(description = "所属行业", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
-    @ExcelProperty(value = "所属行业", converter = DictConvert.class)
-    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY)
-    private Integer industryId;
-
-    @Schema(description = "客户等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
-    @ExcelProperty(value = "客户等级", converter = DictConvert.class)
-    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL)
-    private Integer level;
-
-    @Schema(description = "客户来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
-    @ExcelProperty(value = "客户来源", converter = DictConvert.class)
-    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE)
-    private Integer source;
-
-    @Schema(description = "负责人的用户编号", example = "25682")
+    @Schema(description = "手机", example = "25682")
     @ExcelProperty("手机")
     private String mobile;
 
-    @Schema(description = "负责人的用户编号", example = "25682")
+    @Schema(description = "电话", example = "25682")
     @ExcelProperty("电话")
     private String telephone;
 
-    @Schema(description = "负责人的用户编号", example = "25682")
-    @ExcelProperty("网址")
-    private String website;
-
-    @Schema(description = "负责人的用户编号", example = "25682")
+    @Schema(description = "QQ", example = "25682")
     @ExcelProperty("QQ")
     private String qq;
 
-    @Schema(description = "负责人的用户编号", example = "25682")
+    @Schema(description = "wechat", example = "25682")
     @ExcelProperty("wechat")
     private String wechat;
 
-    @Schema(description = "负责人的用户编号", example = "25682")
+    @Schema(description = "email", example = "25682")
     @ExcelProperty("email")
     private String email;
 
-    @Schema(description = "负责人的用户编号", example = "25682")
-    @ExcelProperty("客户描述")
-    private String description;
-
-    @Schema(description = "负责人的用户编号", example = "25682")
-    @ExcelProperty("备注")
-    private String remark;
-
-    @Schema(description = "负责人的用户编号", example = "25682")
-    @ExcelProperty("负责人的用户编号")
-    private Long ownerUserId;
-    @Schema(description = "负责人名字", example = "25682")
-    @ExcelProperty("负责人名字")
-    private String ownerUserName;
-    @Schema(description = "负责人部门")
-    @ExcelProperty("负责人部门")
-    private String ownerUserDeptName;
-
     @Schema(description = "地区编号", example = "1024")
     @ExcelProperty("地区编号")
     private Integer areaId;
@@ -108,14 +90,24 @@ public class CrmCustomerRespVO {
     @ExcelProperty("详细地址")
     private String detailAddress;
 
-    @Schema(description = "最后跟进时间")
-    @ExcelProperty("最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime contactLastTime;
+    @Schema(description = "所属行业", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    @ExcelProperty(value = "所属行业", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY)
+    private Integer industryId;
 
-    @Schema(description = "下次联系时间")
-    @ExcelProperty("下次联系时间")
-    private LocalDateTime contactNextTime;
+    @Schema(description = "客户等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    @ExcelProperty(value = "客户等级", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL)
+    private Integer level;
+
+    @Schema(description = "客户来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    @ExcelProperty(value = "客户来源", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE)
+    private Integer source;
+
+    @Schema(description = "负责人的用户编号", example = "25682")
+    @ExcelProperty("备注")
+    private String remark;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("创建时间")

+ 25 - 32
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerSaveReqVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerSaveReqVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
 
 import cn.iocoder.yudao.framework.common.validation.InEnum;
 import cn.iocoder.yudao.framework.common.validation.Mobile;
@@ -13,6 +13,7 @@ import com.mzt.logapi.starter.annotation.DiffLogField;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.Email;
 import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
@@ -34,19 +35,14 @@ public class CrmCustomerSaveReqVO {
     @NotEmpty(message = "客户名称不能为空")
     private String name;
 
-    @Schema(description = "所属行业", example = "1")
-    @DiffLogField(name = "所属行业", function = CrmCustomerIndustryParseFunction.NAME)
-    @DictFormat(CRM_CUSTOMER_INDUSTRY)
-    private Integer industryId;
-
-    @Schema(description = "客户等级", example = "2")
-    @DiffLogField(name = "客户等级", function = CrmCustomerLevelParseFunction.NAME)
-    @InEnum(CrmCustomerLevelEnum.class)
-    private Integer level;
+    @Schema(description = "下次联系时间")
+    @DiffLogField(name = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactNextTime;
 
-    @Schema(description = "客户来源", example = "3")
-    @DiffLogField(name = "客户来源", function = CrmCustomerSourceParseFunction.NAME)
-    private Integer source;
+    @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    @NotNull(message = "负责人的用户编号不能为空")
+    private Long ownerUserId;
 
     @Schema(description = "手机", example = "18000000000")
     @DiffLogField(name = "手机")
@@ -58,10 +54,6 @@ public class CrmCustomerSaveReqVO {
     @Telephone
     private String telephone;
 
-    @Schema(description = "网址", example = "https://www.baidu.com")
-    @DiffLogField(name = "网址")
-    private String website;
-
     @Schema(description = "QQ", example = "123456789")
     @DiffLogField(name = "QQ")
     @Size(max = 20, message = "QQ长度不能超过 20 个字符")
@@ -78,15 +70,6 @@ public class CrmCustomerSaveReqVO {
     @Size(max = 255, message = "邮箱长度不能超过 255 个字符")
     private String email;
 
-    @Schema(description = "客户描述", example = "任意文字")
-    @DiffLogField(name = "客户描述")
-    @Size(max = 4096, message = "客户描述长度不能超过 4096 个字符")
-    private String description;
-
-    @Schema(description = "备注", example = "随便")
-    @DiffLogField(name = "备注")
-    private String remark;
-
     @Schema(description = "地区编号", example = "20158")
     @DiffLogField(name = "地区编号", function = SysAreaParseFunction.NAME)
     private Integer areaId;
@@ -95,12 +78,22 @@ public class CrmCustomerSaveReqVO {
     @DiffLogField(name = "详细地址")
     private String detailAddress;
 
-    @Schema(description = "下次联系时间")
-    @DiffLogField(name = "下次联系时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime contactNextTime;
+    @Schema(description = "所属行业", example = "1")
+    @DiffLogField(name = "所属行业", function = CrmCustomerIndustryParseFunction.NAME)
+    @DictFormat(CRM_CUSTOMER_INDUSTRY)
+    private Integer industryId;
 
-    @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
-    private Long ownerUserId;
+    @Schema(description = "客户等级", example = "2")
+    @DiffLogField(name = "客户等级", function = CrmCustomerLevelParseFunction.NAME)
+    @InEnum(CrmCustomerLevelEnum.class)
+    private Integer level;
+
+    @Schema(description = "客户来源", example = "3")
+    @DiffLogField(name = "客户来源", function = CrmCustomerSourceParseFunction.NAME)
+    private Integer source;
+
+    @Schema(description = "备注", example = "随便")
+    @DiffLogField(name = "备注")
+    private String remark;
 
 }

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

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
 
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import io.swagger.v3.oas.annotations.media.Schema;

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

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.business.vo.business.CrmBusinessRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.followup.vo.CrmFollowUpRecordSaveReqVO;
@@ -13,6 +14,8 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.followup.CrmFollowUpRecordDO;
 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.followup.CrmFollowUpRecordService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,7 +29,7 @@ import java.util.ArrayList;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -44,6 +47,9 @@ public class CrmFollowUpRecordController {
     @Resource
     private CrmBusinessService businessService;
 
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @PostMapping("/create")
     @Operation(summary = "创建跟进记录")
     @PreAuthorize("@ss.hasPermission('crm:follow-up-record:create')")
@@ -74,17 +80,24 @@ public class CrmFollowUpRecordController {
     @PreAuthorize("@ss.hasPermission('crm:follow-up-record:query')")
     public CommonResult<PageResult<CrmFollowUpRecordRespVO>> getFollowUpRecordPage(@Valid CrmFollowUpRecordPageReqVO pageReqVO) {
         PageResult<CrmFollowUpRecordDO> pageResult = followUpRecordService.getFollowUpRecordPage(pageReqVO);
-        /// 拼接数据
-        Map<Long, CrmContactDO> contactMap = convertMap(contactService.getContactListByIds(
-                convertSetByFlatMap(pageResult.getList(), item -> item.getContactIds().stream())), CrmContactDO::getId);
-        Map<Long, CrmBusinessDO> businessMap = convertMap(businessService.getBusinessList(
-                convertSetByFlatMap(pageResult.getList(), item -> item.getBusinessIds().stream())), CrmBusinessDO::getId);
+        // 1.1 查询联系人和商机
+        Map<Long, CrmContactDO> contactMap = contactService.getContactMap(
+                convertSetByFlatMap(pageResult.getList(), item -> item.getContactIds().stream()));
+        Map<Long, CrmBusinessDO> businessMap = businessService.getBusinessMap(
+                convertSetByFlatMap(pageResult.getList(), item -> item.getBusinessIds().stream()));
+        // 1.2 查询用户
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), item -> Long.valueOf(item.getCreator())));
+        // 2. 拼接数据
         PageResult<CrmFollowUpRecordRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmFollowUpRecordRespVO.class, record -> {
-            record.setContactNames(new ArrayList<>()).setBusinessNames(new ArrayList<>());
-            record.getContactIds().forEach(id -> MapUtils.findAndThen(contactMap, id,
-                    contact -> record.getContactNames().add(contact.getName())));
-            record.getContactIds().forEach(id -> MapUtils.findAndThen(businessMap, id,
-                    business -> record.getBusinessNames().add(business.getName())));
+            // 2.1 设置联系人和商机信息
+            record.setBusinesses(new ArrayList<>()).setContacts(new ArrayList<>());
+            record.getContactIds().forEach(id -> MapUtils.findAndThen(contactMap, id, contact ->
+                    record.getContacts().add(new CrmBusinessRespVO().setId(contact.getId()).setName(contact.getName()))));
+            record.getContactIds().forEach(id -> MapUtils.findAndThen(businessMap, id, business ->
+                    record.getBusinesses().add(new CrmBusinessRespVO().setId(business.getId()).setName(business.getName()))));
+            // 2.2 设置用户信息
+            MapUtils.findAndThen(userMap, Long.valueOf(record.getCreator()), user -> record.setCreatorName(user.getNickname()));
         });
         return success(voPageResult);
     }

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

@@ -1,7 +1,9 @@
 package cn.iocoder.yudao.module.crm.controller.admin.followup.vo;
 
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
 import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -36,19 +38,26 @@ public class CrmFollowUpRecordRespVO {
 
     @Schema(description = "关联的商机编号数组")
     private List<Long> businessIds;
-    @Schema(description = "关联的商机名称数组")
-    private List<String> businessNames;
+    @Schema(description = "关联的商机数组")
+    private List<CrmBusinessRespVO> businesses;
 
     @Schema(description = "关联的联系人编号数组")
     private List<Long> contactIds;
     @Schema(description = "关联的联系人名称数组")
-    private List<String> contactNames;
+    private List<CrmBusinessRespVO> contacts;
 
     @Schema(description = "图片")
     private List<String> picUrls;
     @Schema(description = "附件")
     private List<String> fileUrls;
 
+    @Schema(description = "创建人", example = "1024")
+    @ExcelProperty("创建人")
+    private String creator;
+    @Schema(description = "创建人名字", example = "芋道源码")
+    @ExcelProperty("创建人名字")
+    private String creatorName;
+
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 

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

@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogV2RespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo.CrmOperateLogRespVO;
 import cn.iocoder.yudao.module.crm.enums.LogRecordConstants;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
@@ -41,7 +41,7 @@ public class CrmOperateLogController {
     private static final Map<Integer, String> BIZ_TYPE_MAP = new HashMap<>();
 
     static {
-        BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_LEADS.getType(), CRM_LEADS_TYPE);
+        BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_CLUE.getType(), CRM_CLUE_TYPE);
         BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_CUSTOMER.getType(), CRM_CUSTOMER_TYPE);
         BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_CONTACT.getType(), CRM_CONTACT_TYPE);
         BIZ_TYPE_MAP.put(CrmBizTypeEnum.CRM_BUSINESS.getType(), CRM_BUSINESS_TYPE);
@@ -54,11 +54,11 @@ public class CrmOperateLogController {
     @GetMapping("/page")
     @Operation(summary = "获得操作日志")
     @PreAuthorize("@ss.hasPermission('crm:operate-log:query')")
-    public CommonResult<PageResult<CrmOperateLogV2RespVO>> getCustomerOperateLog(@Valid CrmOperateLogPageReqVO pageReqVO) {
+    public CommonResult<PageResult<CrmOperateLogRespVO>> getCustomerOperateLog(@Valid CrmOperateLogPageReqVO pageReqVO) {
         OperateLogV2PageReqDTO reqDTO = new OperateLogV2PageReqDTO();
         reqDTO.setPageSize(PAGE_SIZE_NONE); // 默认不分页,需要分页需注释
         reqDTO.setBizType(BIZ_TYPE_MAP.get(pageReqVO.getBizType())).setBizId(pageReqVO.getBizId());
-        return success(BeanUtils.toBean(operateLogApi.getOperateLogPage(reqDTO), CrmOperateLogV2RespVO.class));
+        return success(BeanUtils.toBean(operateLogApi.getOperateLogPage(reqDTO), CrmOperateLogRespVO.class));
     }
 
 }

+ 2 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogV2RespVO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java

@@ -6,10 +6,10 @@ import lombok.Data;
 
 import java.time.LocalDateTime;
 
-@Schema(description = "管理后台 - CRM 跟进 Response VO")
+@Schema(description = "管理后台 - CRM 操作日志 Response VO")
 @Data
 @ExcelIgnoreUnannotated
-public class CrmOperateLogV2RespVO {
+public class CrmOperateLogRespVO {
 
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
     private Long id;

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

@@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.crm.controller.admin.permission;
 import cn.hutool.core.collection.CollUtil;
 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.CrmPermissionUpdateReqVO;
-import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
 import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
@@ -19,6 +19,7 @@ import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import com.google.common.collect.Multimaps;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.Parameters;
@@ -29,11 +30,16 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - CRM 数据权限")
@@ -98,18 +104,32 @@ public class CrmPermissionController {
     @PreAuthorize("@ss.hasPermission('crm:permission:query')")
     public CommonResult<List<CrmPermissionRespVO>> getPermissionList(@RequestParam("bizType") Integer bizType,
                                                                      @RequestParam("bizId") Long bizId) {
-        List<CrmPermissionDO> permission = permissionService.getPermissionListByBiz(bizType, bizId);
-        if (CollUtil.isEmpty(permission)) {
+        List<CrmPermissionDO> permissions = permissionService.getPermissionListByBiz(bizType, bizId);
+        if (CollUtil.isEmpty(permissions)) {
             return success(Collections.emptyList());
         }
 
+        // 查询相关数据
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(permissions, CrmPermissionDO::getUserId));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        Map<Long, PostRespDTO> postMap = postApi.getPostMap(
+                convertSetByFlatMap(userMap.values(), AdminUserRespDTO::getPostIds,
+                        item -> item != null ? item.stream() : Stream.empty()));
         // 拼接数据
-        List<AdminUserRespDTO> userList = adminUserApi.getUserList(convertSet(permission, CrmPermissionDO::getUserId));
-        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userList, AdminUserRespDTO::getDeptId));
-        Set<Long> postIds = CollectionUtils.convertSetByFlatMap(userList, AdminUserRespDTO::getPostIds,
-                item -> item != null ? item.stream() : Stream.empty());
-        Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
-        return success(CrmPermissionConvert.INSTANCE.convert(permission, userList, deptMap, postMap));
+        return success(CollectionUtils.convertList(BeanUtils.toBean(permissions, CrmPermissionRespVO.class), item -> {
+            findAndThen(userMap, item.getUserId(), user -> {
+                item.setNickname(user.getNickname());
+                findAndThen(deptMap, user.getDeptId(), deptRespDTO -> item.setDeptName(deptRespDTO.getName()));
+                if (CollUtil.isEmpty(user.getPostIds())) {
+                    item.setPostNames(Collections.emptySet());
+                    return;
+                }
+                List<PostRespDTO> postList = MapUtils.getList(Multimaps.forMap(postMap), user.getPostIds());
+                item.setPostNames(CollectionUtils.convertSet(postList, PostRespDTO::getName));
+            });
+            return item;
+        }));
     }
 
 }

+ 36 - 17
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java

@@ -1,16 +1,17 @@
 package cn.iocoder.yudao.module.crm.controller.admin.product;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.product.vo.product.CrmProductPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.product.vo.product.CrmProductRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.product.vo.product.CrmProductSaveReqVO;
-import cn.iocoder.yudao.module.crm.convert.product.CrmProductConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductDO;
 import cn.iocoder.yudao.module.crm.service.product.CrmProductCategoryService;
@@ -34,10 +35,9 @@ import java.util.Map;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
-import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static java.util.Collections.singletonList;
 
 @Tag(name = "管理后台 - CRM 产品")
 @RestController
@@ -49,6 +49,7 @@ public class CrmProductController {
     private CrmProductService productService;
     @Resource
     private CrmProductCategoryService productCategoryService;
+
     @Resource
     private AdminUserApi adminUserApi;
 
@@ -82,21 +83,30 @@ public class CrmProductController {
     @PreAuthorize("@ss.hasPermission('crm:product:query')")
     public CommonResult<CrmProductRespVO> getProduct(@RequestParam("id") Long id) {
         CrmProductDO product = productService.getProduct(id);
+        return success(buildProductDetail(product));
+    }
+
+    private CrmProductRespVO buildProductDetail(CrmProductDO product) {
         if (product == null) {
-            return success(null);
+            return null;
         }
-        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                SetUtils.asSet(Long.valueOf(product.getCreator()), product.getOwnerUserId()));
-        CrmProductCategoryDO category = productCategoryService.getProductCategory(product.getCategoryId());
-        return success(CrmProductConvert.INSTANCE.convert(product, userMap, category));
+        return buildProductDetailList(singletonList(product)).get(0);
+    }
+
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得产品精简列表", description = "只包含被开启的产品,主要用于前端的下拉选项")
+    public CommonResult<List<CrmProductRespVO>> getProductSimpleList() {
+        List<CrmProductDO> list = productService.getProductListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(convertList(list, product -> new CrmProductRespVO().setId(product.getId()).setName(product.getName())
+                .setUnit(product.getUnit()).setNo(product.getNo()).setPrice(product.getPrice())));
     }
 
     @GetMapping("/page")
     @Operation(summary = "获得产品分页")
     @PreAuthorize("@ss.hasPermission('crm:product:query')")
     public CommonResult<PageResult<CrmProductRespVO>> getProductPage(@Valid CrmProductPageReqVO pageVO) {
-        PageResult<CrmProductDO> pageResult = productService.getProductPage(pageVO, getLoginUserId());
-        return success(new PageResult<>(getProductDetailList(pageResult.getList()), pageResult.getTotal()));
+        PageResult<CrmProductDO> pageResult = productService.getProductPage(pageVO);
+        return success(new PageResult<>(buildProductDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/export-excel")
@@ -106,21 +116,30 @@ public class CrmProductController {
     public void exportProductExcel(@Valid CrmProductPageReqVO exportReqVO,
                                    HttpServletResponse response) throws IOException {
         exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
-        List<CrmProductDO> list = productService.getProductPage(exportReqVO, getLoginUserId()).getList();
+        List<CrmProductDO> list = productService.getProductPage(exportReqVO).getList();
         // 导出 Excel
         ExcelUtils.write(response, "产品.xls", "数据", CrmProductRespVO.class,
-                getProductDetailList(list));
+                buildProductDetailList(list));
     }
 
-    private List<CrmProductRespVO> getProductDetailList(List<CrmProductDO> list) {
+    private List<CrmProductRespVO> buildProductDetailList(List<CrmProductDO> list) {
         if (CollUtil.isEmpty(list)) {
             return Collections.emptyList();
         }
+        // 1.1 获得用户信息
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                 convertSetByFlatMap(list, user -> Stream.of(Long.valueOf(user.getCreator()), user.getOwnerUserId())));
-        List<CrmProductCategoryDO> productCategoryList = productCategoryService.getProductCategoryList(
+        // 1.2 获得分类信息
+        Map<Long, CrmProductCategoryDO> categoryMap = productCategoryService.getProductCategoryMap(
                 convertSet(list, CrmProductDO::getCategoryId));
-        return CrmProductConvert.INSTANCE.convertList(list, userMap, productCategoryList);
+        // 2. 拼接数据
+        return BeanUtils.toBean(list, CrmProductRespVO.class, productVO -> {
+            // 2.1 设置用户信息
+            MapUtils.findAndThen(userMap, productVO.getOwnerUserId(), user -> productVO.setOwnerUserName(user.getNickname()));
+            MapUtils.findAndThen(userMap, Long.valueOf(productVO.getCreator()), user -> productVO.setCreatorName(user.getNickname()));
+            // 2.2 设置分类名称
+            MapUtils.findAndThen(categoryMap, productVO.getCategoryId(), category -> productVO.setCategoryName(category.getName()));
+        });
     }
 
 }

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

@@ -8,6 +8,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 @Schema(description = "管理后台 - CRM 产品 Response VO")
@@ -34,7 +35,7 @@ public class CrmProductRespVO {
 
     @Schema(description = "价格, 单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
     @ExcelProperty("价格,单位:分")
-    private Long price;
+    private BigDecimal price;
 
     @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "上架")
     @ExcelProperty(value = "单位", converter = DictConvert.class)

+ 4 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductSaveReqVO.java

@@ -7,6 +7,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
+import java.math.BigDecimal;
+
 @Schema(description = "管理后台 - CRM 产品创建/修改 Request VO")
 @Data
 public class CrmProductSaveReqVO {
@@ -28,10 +30,10 @@ public class CrmProductSaveReqVO {
     @DiffLogField(name = "单位", function = CrmProductUnitParseFunction.NAME)
     private Integer unit;
 
-    @Schema(description = "价格, 单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
+    @Schema(description = "价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
     @NotNull(message = "价格不能为空")
     @DiffLogField(name = "价格")
-    private Long price;
+    private BigDecimal price;
 
     @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "上架")
     @NotNull(message = "状态不能为空")

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

@@ -4,20 +4,23 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 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.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivablePageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableUpdateReqVO;
-import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivableConvert;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableSaveReqVO;
 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.receivable.CrmReceivableDO;
 import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
@@ -31,14 +34,15 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
-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.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -57,18 +61,20 @@ public class CrmReceivableController {
 
     @Resource
     private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建回款")
     @PreAuthorize("@ss.hasPermission('crm:receivable:create')")
-    public CommonResult<Long> createReceivable(@Valid @RequestBody CrmReceivableCreateReqVO createReqVO) {
-        return success(receivableService.createReceivable(createReqVO, getLoginUserId()));
+    public CommonResult<Long> createReceivable(@Valid @RequestBody CrmReceivableSaveReqVO createReqVO) {
+        return success(receivableService.createReceivable(createReqVO));
     }
 
     @PutMapping("/update")
     @Operation(summary = "更新回款")
     @PreAuthorize("@ss.hasPermission('crm:receivable:update')")
-    public CommonResult<Boolean> updateReceivable(@Valid @RequestBody CrmReceivableUpdateReqVO updateReqVO) {
+    public CommonResult<Boolean> updateReceivable(@Valid @RequestBody CrmReceivableSaveReqVO updateReqVO) {
         receivableService.updateReceivable(updateReqVO);
         return success(true);
     }
@@ -88,7 +94,14 @@ public class CrmReceivableController {
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
     public CommonResult<CrmReceivableRespVO> getReceivable(@RequestParam("id") Long id) {
         CrmReceivableDO receivable = receivableService.getReceivable(id);
-        return success(CrmReceivableConvert.INSTANCE.convert(receivable));
+        return success(buildReceivableDetail(receivable));
+    }
+
+    private CrmReceivableRespVO buildReceivableDetail(CrmReceivableDO receivable) {
+        if (receivable == null) {
+            return null;
+        }
+        return buildReceivableDetailList(Collections.singletonList(receivable)).get(0);
     }
 
     @GetMapping("/page")
@@ -96,7 +109,7 @@ public class CrmReceivableController {
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
     public CommonResult<PageResult<CrmReceivableRespVO>> getReceivablePage(@Valid CrmReceivablePageReqVO pageReqVO) {
         PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePage(pageReqVO, getLoginUserId());
-        return success(buildReceivableDetailPage(pageResult));
+        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/page-by-customer")
@@ -104,10 +117,9 @@ public class CrmReceivableController {
     public CommonResult<PageResult<CrmReceivableRespVO>> getReceivablePageByCustomer(@Valid CrmReceivablePageReqVO pageReqVO) {
         Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePageByCustomerId(pageReqVO);
-        return success(buildReceivableDetailPage(pageResult));
+        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
-    // TODO 芋艿:后面在优化导出
     @GetMapping("/export-excel")
     @Operation(summary = "导出回款 Excel")
     @PreAuthorize("@ss.hasPermission('crm:receivable:export')")
@@ -115,33 +127,56 @@ public class CrmReceivableController {
     public void exportReceivableExcel(@Valid CrmReceivablePageReqVO exportReqVO,
                                       HttpServletResponse response) throws IOException {
         exportReqVO.setPageSize(PAGE_SIZE_NONE);
-        PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePage(exportReqVO, getLoginUserId());
+        List<CrmReceivableDO> list = receivableService.getReceivablePage(exportReqVO, getLoginUserId()).getList();
         // 导出 Excel
         ExcelUtils.write(response, "回款.xls", "数据", CrmReceivableRespVO.class,
-                buildReceivableDetailPage(pageResult).getList());
+                buildReceivableDetailList(list));
     }
 
-    /**
-     * 构建详细的回款分页结果
-     *
-     * @param pageResult 简单的回款分页结果
-     * @return 详细的回款分页结果
-     */
-    private PageResult<CrmReceivableRespVO> buildReceivableDetailPage(PageResult<CrmReceivableDO> pageResult) {
-        List<CrmReceivableDO> receivableList = pageResult.getList();
+    private List<CrmReceivableRespVO> buildReceivableDetailList(List<CrmReceivableDO> receivableList) {
         if (CollUtil.isEmpty(receivableList)) {
-            return PageResult.empty(pageResult.getTotal());
+            return Collections.emptyList();
         }
-        // 1. 获取客户列表
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(
+        // 1.1 获取客户列表
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                 convertSet(receivableList, CrmReceivableDO::getCustomerId));
-        // 2. 获取创建人、负责人列表
+        // 1.2 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(receivableList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
-        // 3. 获得合同列表
-        List<CrmContractDO> contractList = contractService.getContractList(
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        // 1.3 获得合同列表
+        Map<Long, CrmContractDO> contractMap = contractService.getContractMap(
                 convertSet(receivableList, CrmReceivableDO::getContractId));
-        return CrmReceivableConvert.INSTANCE.convertPage(pageResult, userMap, customerList, contractList);
+        // 2. 拼接结果
+        return BeanUtils.toBean(receivableList, CrmReceivableRespVO.class, (receivableVO) -> {
+            // 2.1 拼接客户名称
+            findAndThen(customerMap, receivableVO.getCustomerId(), customer -> receivableVO.setCustomerName(customer.getName()));
+            // 2.2 拼接负责人、创建人名称
+            MapUtils.findAndThen(userMap, NumberUtils.parseLong(receivableVO.getCreator()),
+                    user -> receivableVO.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, receivableVO.getOwnerUserId(), user -> {
+                receivableVO.setOwnerUserName(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> receivableVO.setOwnerUserDeptName(dept.getName()));
+            });
+            // 2.3 拼接合同信息
+            findAndThen(contractMap, receivableVO.getContractId(), contract ->
+                    receivableVO.setContract(BeanUtils.toBean(contract, CrmContractRespVO.class)));
+        });
+    }
+
+    @PutMapping("/submit")
+    @Operation(summary = "提交回款审批")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:update')")
+    public CommonResult<Boolean> submitContract(@RequestParam("id") Long id) {
+        receivableService.submitReceivable(id, getLoginUserId());
+        return success(true);
+    }
+
+    @GetMapping("/audit-count")
+    @Operation(summary = "获得待审核回款数量")
+    @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
+    public CommonResult<Long> getAuditReceivableCount() {
+        return success(receivableService.getAuditReceivableCount(getLoginUserId()));
     }
 
 }

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

@@ -5,13 +5,13 @@ import cn.hutool.core.lang.Assert;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanPageReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanUpdateReqVO;
-import cn.iocoder.yudao.module.crm.convert.receivable.CrmReceivablePlanConvert;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanSaveReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
 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.receivable.CrmReceivableDO;
@@ -24,24 +24,25 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.Valid;
-import org.springframework.context.annotation.Lazy;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
-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.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
@@ -56,7 +57,6 @@ public class CrmReceivablePlanController {
     @Resource
     private CrmReceivableService receivableService;
     @Resource
-    @Lazy
     private CrmContractService contractService;
     @Resource
     private CrmCustomerService customerService;
@@ -67,14 +67,14 @@ public class CrmReceivablePlanController {
     @PostMapping("/create")
     @Operation(summary = "创建回款计划")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:create')")
-    public CommonResult<Long> createReceivablePlan(@Valid @RequestBody CrmReceivablePlanCreateReqVO createReqVO) {
-        return success(receivablePlanService.createReceivablePlan(createReqVO, getLoginUserId()));
+    public CommonResult<Long> createReceivablePlan(@Valid @RequestBody CrmReceivablePlanSaveReqVO createReqVO) {
+        return success(receivablePlanService.createReceivablePlan(createReqVO));
     }
 
     @PutMapping("/update")
     @Operation(summary = "更新回款计划")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:update')")
-    public CommonResult<Boolean> updateReceivablePlan(@Valid @RequestBody CrmReceivablePlanUpdateReqVO updateReqVO) {
+    public CommonResult<Boolean> updateReceivablePlan(@Valid @RequestBody CrmReceivablePlanSaveReqVO updateReqVO) {
         receivablePlanService.updateReceivablePlan(updateReqVO);
         return success(true);
     }
@@ -94,7 +94,14 @@ public class CrmReceivablePlanController {
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
     public CommonResult<CrmReceivablePlanRespVO> getReceivablePlan(@RequestParam("id") Long id) {
         CrmReceivablePlanDO receivablePlan = receivablePlanService.getReceivablePlan(id);
-        return success(CrmReceivablePlanConvert.INSTANCE.convert(receivablePlan));
+        return success(buildReceivablePlanDetail(receivablePlan));
+    }
+
+    private CrmReceivablePlanRespVO buildReceivablePlanDetail(CrmReceivablePlanDO receivablePlan) {
+        if (receivablePlan == null) {
+            return null;
+        }
+        return buildReceivableDetailList(Collections.singletonList(receivablePlan)).get(0);
     }
 
     @GetMapping("/page")
@@ -102,7 +109,7 @@ public class CrmReceivablePlanController {
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
     public CommonResult<PageResult<CrmReceivablePlanRespVO>> getReceivablePlanPage(@Valid CrmReceivablePlanPageReqVO pageReqVO) {
         PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(pageReqVO, getLoginUserId());
-        return success(convertDetailReceivablePlanPage(pageResult));
+        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
     @GetMapping("/page-by-customer")
@@ -110,10 +117,9 @@ public class CrmReceivablePlanController {
     public CommonResult<PageResult<CrmReceivablePlanRespVO>> getReceivablePlanPageByCustomer(@Valid CrmReceivablePlanPageReqVO pageReqVO) {
         Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPageByCustomerId(pageReqVO);
-        return success(convertDetailReceivablePlanPage(pageResult));
+        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
     }
 
-    // TODO 芋艿:后面在优化导出
     @GetMapping("/export-excel")
     @Operation(summary = "导出回款计划 Excel")
     @PreAuthorize("@ss.hasPermission('crm:receivable-plan:export')")
@@ -121,36 +127,64 @@ public class CrmReceivablePlanController {
     public void exportReceivablePlanExcel(@Valid CrmReceivablePlanPageReqVO exportReqVO,
                                           HttpServletResponse response) throws IOException {
         exportReqVO.setPageSize(PAGE_SIZE_NONE);
-        PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPage(exportReqVO, getLoginUserId());
+        List<CrmReceivablePlanDO> list = receivablePlanService.getReceivablePlanPage(exportReqVO, getLoginUserId()).getList();
         // 导出 Excel
         ExcelUtils.write(response, "回款计划.xls", "数据", CrmReceivablePlanRespVO.class,
-                convertDetailReceivablePlanPage(pageResult).getList());
+                buildReceivableDetailList(list));
     }
 
-    /**
-     * 构建详细的回款计划分页结果
-     *
-     * @param pageResult 简单的回款计划分页结果
-     * @return 详细的回款计划分页结果
-     */
-    private PageResult<CrmReceivablePlanRespVO> convertDetailReceivablePlanPage(PageResult<CrmReceivablePlanDO> pageResult) {
-        List<CrmReceivablePlanDO> receivablePlanList = pageResult.getList();
+    private List<CrmReceivablePlanRespVO> buildReceivableDetailList(List<CrmReceivablePlanDO> receivablePlanList) {
         if (CollUtil.isEmpty(receivablePlanList)) {
-            return PageResult.empty(pageResult.getTotal());
+            return Collections.emptyList();
         }
-        // 1. 获取客户列表
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(
+        // 1.1 获取客户 Map
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                 convertSet(receivablePlanList, CrmReceivablePlanDO::getCustomerId));
-        // 2. 获取创建人、负责人列表
+        // 1.2 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(receivablePlanList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
-        // 3. 获得合同列表
-        List<CrmContractDO> contractList = contractService.getContractList(
+        // 1.3 获得合同 Map
+        Map<Long, CrmContractDO> contractMap = contractService.getContractMap(
                 convertSet(receivablePlanList, CrmReceivablePlanDO::getContractId));
-        // 4. 获得还款列表
-        List<CrmReceivableDO> receivableList = receivableService.getReceivableList(
+        // 1.4 获得回款 Map
+        Map<Long, CrmReceivableDO> receivableMap = receivableService.getReceivableMap(
                 convertSet(receivablePlanList, CrmReceivablePlanDO::getReceivableId));
-        return CrmReceivablePlanConvert.INSTANCE.convertPage(pageResult, userMap, customerList, contractList, receivableList);
+        // 2. 拼接数据
+        return BeanUtils.toBean(receivablePlanList, CrmReceivablePlanRespVO.class, (receivablePlanVO) -> {
+            // 2.1 拼接客户信息
+            findAndThen(customerMap, receivablePlanVO.getCustomerId(), customer -> receivablePlanVO.setCustomerName(customer.getName()));
+            // 2.2 拼接用户信息
+            findAndThen(userMap, receivablePlanVO.getOwnerUserId(), user -> receivablePlanVO.setOwnerUserName(user.getNickname()));
+            findAndThen(userMap, Long.parseLong(receivablePlanVO.getCreator()), user -> receivablePlanVO.setCreatorName(user.getNickname()));
+            // 2.3 拼接合同信息
+            findAndThen(contractMap, receivablePlanVO.getContractId(), contract -> receivablePlanVO.setContractNo(contract.getNo()));
+            // 2.4 拼接回款信息
+            receivablePlanVO.setReceivable(BeanUtils.toBean(receivableMap.get(receivablePlanVO.getReceivableId()), CrmReceivableRespVO.class));
+        });
+    }
+
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得回款计划精简列表", description = "获得回款计划精简列表,主要用于前端的下拉选项")
+    @Parameters({
+            @Parameter(name = "customerId", description = "客户编号", required = true),
+            @Parameter(name = "contractId", description = "合同编号", required = true)
+    })
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
+    public CommonResult<List<CrmReceivablePlanRespVO>> getReceivablePlanSimpleList(@RequestParam("customerId") Long customerId,
+                                                                                   @RequestParam("contractId") Long contractId) {
+        CrmReceivablePlanPageReqVO pageReqVO = new CrmReceivablePlanPageReqVO().setCustomerId(customerId).setContractId(contractId);
+        pageReqVO.setPageNo(PAGE_SIZE_NONE);
+        PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPageByCustomerId(pageReqVO);
+        return success(convertList(pageResult.getList(), receivablePlan -> new CrmReceivablePlanRespVO() // 只返回 id、period 等信息
+                .setId(receivablePlan.getId()).setPeriod(receivablePlan.getPeriod()).setReceivableId(receivablePlan.getReceivableId())
+                .setPrice(receivablePlan.getPrice()).setReturnType(receivablePlan.getReturnType())));
+    }
+
+    @GetMapping("/remind-count")
+    @Operation(summary = "获得待回款提醒数量")
+    @PreAuthorize("@ss.hasPermission('crm:receivable-plan:query')")
+    public CommonResult<Long> getReceivablePlanRemindCount() {
+        return success(receivablePlanService.getReceivablePlanRemindCount(getLoginUserId()));
     }
 
 }

+ 0 - 54
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanBaseVO.java

@@ -1,54 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import org.springframework.format.annotation.DateTimeFormat;
-
-import java.time.LocalDateTime;
-
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-
-/**
- * 回款计划 Base VO,提供给添加、修改、详细的子 VO 使用
- * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
- */
-@Data
-public class CrmReceivablePlanBaseVO {
-
-    @Schema(description = "期数", example = "1")
-    private Integer period;
-
-    @Schema(description = "回款计划编号", example = "19852")
-    private Long receivableId;
-
-    @Schema(description = "计划回款金额", example = "29675")
-    private Integer price;
-
-    @Schema(description = "计划回款日期")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime returnTime;
-
-    @Schema(description = "提前几天提醒")
-    private Integer remindDays;
-
-    @Schema(description = "提醒日期")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime remindTime;
-
-    @Schema(description = "客户名称", example = "18026")
-    private Long customerId;
-
-    @Schema(description = "合同编号", example = "3473")
-    private Long contractId;
-
-    // TODO @liuhongfeng:负责人编号
-    @Schema(description = "负责人编号", example = "17828")
-    private Long ownerUserId;
-
-    @Schema(description = "显示顺序")
-    private Integer sort;
-
-    @Schema(description = "备注", example = "备注")
-    private String remark;
-
-}

+ 0 - 12
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanCreateReqVO.java

@@ -1,12 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan;
-
-import lombok.*;
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@Schema(description = "管理后台 - CRM 回款计划创建 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmReceivablePlanCreateReqVO extends CrmReceivablePlanBaseVO {
-
-}

+ 4 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanPageReqVO.java

@@ -30,8 +30,10 @@ public class CrmReceivablePlanPageReqVO extends PageParam {
     @Schema(description = "客户编号", example = "18026")
     private Long customerId;
 
-    // TODO @芋艿:这个搜的应该是合同编号 no
-    @Schema(description = "合同名称", example = "3473")
+    @Schema(description = "合同编号", example = "H3473")
+    private String contractNo;
+
+    @Schema(description = "合同编号", example = "3473")
     private Long contractId;
 
     @Schema(description = "场景类型", example = "1")

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

@@ -1,40 +1,75 @@
 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.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
+import lombok.Data;
+
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
+// TODO @puhui999:缺导出
 @Schema(description = "管理后台 - CRM 回款计划 Response VO")
 @Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmReceivablePlanRespVO extends CrmReceivablePlanBaseVO {
+public class CrmReceivablePlanRespVO {
 
-    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25153")
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     private Long id;
 
-    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
-    private LocalDateTime createTime;
+    @Schema(description = "期数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer period;
 
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Long customerId;
     @Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
     private String customerName;
 
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Long contractId;
     @Schema(description = "合同编号", example = "Q110")
     private String contractNo;
 
+    @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Long ownerUserId;
     @Schema(description = "负责人", example = "test")
     private String ownerUserName;
 
-    @Schema(description = "创建人", example = "25682")
-    private String creator;
+    @Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    private LocalDateTime returnTime;
 
-    @Schema(description = "创建人名字", example = "test")
-    private String creatorName;
+    @Schema(description = "计划回款方式", example = "1")
+    private Integer returnType;
 
-    @Schema(description = "完成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    private Boolean finishStatus;
+    @Schema(description = "计划回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+    private BigDecimal price;
 
-    @Schema(description = "回款方式", example = "1") // 来自 Receivable 的 returnType 字段
-    private Integer returnType;
+    @Schema(description = "回款编号", example = "19852")
+    private Long receivableId;
+    @Schema(description = "回款信息")
+    private CrmReceivableRespVO receivable;
+
+    @Schema(description = "提前几天提醒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer remindDays;
+
+    @Schema(description = "提醒日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    private LocalDateTime remindTime;
+
+    @Schema(description = "备注", example = "备注")
+    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 = "1024")
+    @ExcelProperty("创建人")
+    private String creator;
+    @Schema(description = "创建人名字", example = "芋道源码")
+    @ExcelProperty("创建人名字")
+    private String creatorName;
 
 }

+ 45 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanSaveReqVO.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - CRM 回款计划新增/修改 Request VO")
+@Data
+public class CrmReceivablePlanSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Long id;
+
+    @Schema(description = "客户编号", hidden = true, example = "2")
+    private Long customerId; // 该字段不通过前端传递,而是 contractId 查询出来设置进去
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "合同编号不能为空")
+    private Long contractId;
+
+    @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "负责人编号不能为空")
+    private Long ownerUserId;
+
+    @Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    @NotNull(message = "计划回款日期不能为空")
+    private LocalDateTime returnTime;
+
+    @Schema(description = "回款方式", example = "1")
+    private Integer returnType;
+
+    @Schema(description = "计划回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+    @NotNull(message = "计划回款金额不能为空")
+    private BigDecimal price;
+
+    @Schema(description = "提前几天提醒", example = "1")
+    private Integer remindDays;
+
+    @Schema(description = "备注", example = "备注")
+    private String remark;
+
+}

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

@@ -1,18 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-
-import jakarta.validation.constraints.*;
-
-@Schema(description = "管理后台 - CRM 回款计划更新 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmReceivablePlanUpdateReqVO extends CrmReceivablePlanBaseVO {
-
-    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25153")
-    @NotNull(message = "ID不能为空")
-    private Long id;
-
-}

+ 0 - 61
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableBaseVO.java

@@ -1,61 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable;
-
-import cn.iocoder.yudao.framework.common.validation.InEnum;
-import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-import org.springframework.format.annotation.DateTimeFormat;
-
-import java.time.LocalDateTime;
-
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-
-/**
- * 回款 Base VO,提供给添加、修改、详细的子 VO 使用
- * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
- */
-@Data
-public class CrmReceivableBaseVO {
-
-    @Schema(description = "回款编号",requiredMode = Schema.RequiredMode.REQUIRED, example = "31177")
-    private String no;
-
-    // TODO @liuhongfeng:回款计划编号
-    @Schema(description = "回款计划", example = "31177")
-    private Long planId;
-
-    // TODO @liuhongfeng:客户编号
-    @Schema(description = "客户名称", example = "4963")
-    private Long customerId;
-
-    // TODO @liuhongfeng:客户编号
-    @Schema(description = "合同名称", example = "30305")
-    private Long contractId;
-
-    // TODO @liuhongfeng:这个字段,应该不是前端传递的噢,而是后端自己生成的
-    @Schema(description = "审批状态", example = "1")
-    @InEnum(CrmAuditStatusEnum.class)
-    private Integer checkStatus;
-
-    @Schema(description = "回款日期")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime returnTime;
-
-    @Schema(description = "回款方式", example = "2")
-    private Integer returnType;
-
-    @Schema(description = "回款金额,单位:分", example = "31859")
-    private Integer price;
-
-    // TODO @liuhongfeng:负责人编号
-    @Schema(description = "负责人", example = "22202")
-    private Long ownerUserId;
-
-    @Schema(description = "显示顺序")
-    private Integer sort;
-
-    @Schema(description = "备注", example = "备注")
-    private String remark;
-
-
-}

+ 0 - 12
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableCreateReqVO.java

@@ -1,12 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable;
-
-import lombok.*;
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@Schema(description = "管理后台 - CRM 回款创建 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmReceivableCreateReqVO extends CrmReceivableBaseVO {
-
-}

+ 3 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivablePageReqVO.java

@@ -24,6 +24,9 @@ public class CrmReceivablePageReqVO extends PageParam {
     @Schema(description = "客户编号", example = "4963")
     private Long customerId;
 
+    @Schema(description = "合同编号", example = "4963")
+    private Long contractId;
+
     @Schema(description = "场景类型", example = "1")
     @InEnum(CrmSceneTypeEnum.class)
     private Integer sceneType; // 场景类型,为 null 时则表示全部

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

@@ -1,37 +1,74 @@
 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.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
+import lombok.Data;
+
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 // TODO 芋艿:导出的 VO,可以考虑使用 @Excel 注解,实现导出功能
 @Schema(description = "管理后台 - CRM 回款 Response VO")
 @Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmReceivableRespVO extends CrmReceivableBaseVO {
+public class CrmReceivableRespVO {
 
-    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
+    @Schema(description = "编号", example = "25787")
     private Long id;
 
-    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
-    private LocalDateTime createTime;
+    @Schema(description = "回款编号", example = "31177")
+    private String no;
+
+    @Schema(description = "回款计划编号", example = "1024")
+    private Long planId;
 
+    @Schema(description = "回款方式", example = "2")
+    private Integer returnType;
+
+    @Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+    private BigDecimal price;
+
+    @Schema(description = "计划回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    private LocalDateTime returnTime;
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Long customerId;
     @Schema(description = "客户名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "test")
     private String customerName;
 
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Long contractId;
+    @Schema(description = "合同信息")
+    private CrmContractRespVO contract;
+
+    @Schema(description = "负责人的用户编号", example = "25682")
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+    @Schema(description = "负责人名字", example = "25682")
+    @ExcelProperty("负责人名字")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    @ExcelProperty("负责人部门")
+    private String ownerUserDeptName;
+
+    @Schema(description = "工作流编号", example = "1043")
+    @ExcelProperty("工作流编号")
+    private String processInstanceId;
+
     @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
     private Integer auditStatus;
 
-    @Schema(description = "合同编号", example = "Q110")
-    private String contractNo;
+    @Schema(description = "备注", example = "备注")
+    private String remark;
 
-    @Schema(description = "负责人", example = "test")
-    private String ownerUserName;
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime updateTime;
 
     @Schema(description = "创建人", example = "25682")
     private String creator;
-
     @Schema(description = "创建人名字", example = "test")
     private String creatorName;
 

+ 58 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableSaveReqVO.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.crm.enums.receivable.CrmReceivableReturnTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.operatelog.core.*;
+import com.mzt.logapi.starter.annotation.DiffLogField;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - CRM 回款新增/修改 Request VO")
+@Data
+public class CrmReceivableSaveReqVO {
+
+    @Schema(description = "编号", example = "25787")
+    private Long id;
+
+    @Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @DiffLogField(name = "负责人", function = SysAdminUserParseFunction.NAME)
+    @NotNull(message = "负责人编号不能为空")
+    private Long ownerUserId;
+
+    @Schema(description = "客户编号", example = "2")
+    @DiffLogField(name = "客户", function = CrmCustomerParseFunction.NAME)
+    private Long customerId; // 该字段不通过前端传递,而是 contractId 查询出来设置进去
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @DiffLogField(name = "合同", function = CrmContractParseFunction.NAME)
+    @NotNull(message = "合同编号不能为空")
+    private Long contractId;
+
+    @Schema(description = "回款计划编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @DiffLogField(name = "合同", function = CrmReceivablePlanParseFunction.NAME)
+    private Long planId;
+
+    @Schema(description = "回款方式", example = "2")
+    @DiffLogField(name = "回款方式", function = CrmReceivableReturnTypeParseFunction.NAME)
+    @InEnum(CrmReceivableReturnTypeEnum.class)
+    private Integer returnType;
+
+    @Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "9000")
+    @DiffLogField(name = "回款金额")
+    @NotNull(message = "回款金额不能为空")
+    private BigDecimal price;
+
+    @Schema(description = "回款日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
+    @NotNull(message = "回款日期不能为空")
+    @DiffLogField(name = "回款日期")
+    private LocalDateTime returnTime;
+
+    @Schema(description = "备注", example = "备注")
+    @DiffLogField(name = "备注")
+    private String remark;
+
+}

+ 0 - 18
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableUpdateReqVO.java

@@ -1,18 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-
-import jakarta.validation.constraints.*;
-
-@Schema(description = "管理后台 - CRM 回款更新 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class CrmReceivableUpdateReqVO extends CrmReceivableBaseVO {
-
-    @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25787")
-    @NotNull(message = "ID不能为空")
-    private Long id;
-
-}

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

@@ -0,0 +1,9 @@
+### 合同金额排行榜
+GET {{baseUrl}}/crm/statistics-rank/get-contract-price-rank?deptId=100&times[0]=2022-12-12 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 回款金额排行榜
+GET {{baseUrl}}/crm/statistics-rank/get-receivable-price-rank?deptId=100&times[0]=2022-12-12 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

Some files were not shown because too many files changed in this diff