浏览代码

Merge remote-tracking branch 'upstream/master'

dhb52 1 年之前
父节点
当前提交
edea17e781
共有 100 个文件被更改,包括 3717 次插入45 次删除
  1. 1 0
      pom.xml
  2. 1 0
      sql/mysql/crm.sql
  3. 20 0
      sql/mysql/crm_data.sql
  4. 88 0
      sql/mysql/crm_menu.sql
  5. 8 0
      sql/mysql/pay_wallet.sql
  6. 3 3
      sql/mysql/ruoyi-vue-pro.sql
  7. 4 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java
  8. 18 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  9. 28 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java
  10. 25 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java
  11. 9 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  12. 15 2
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
  13. 19 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  14. 77 31
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
  15. 6 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
  16. 7 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  17. 4 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
  18. 40 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
  19. 8 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  20. 12 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java
  21. 5 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java
  22. 8 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java
  23. 13 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java
  24. 16 5
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
  25. 0 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  26. 25 0
      yudao-module-crm/pom.xml
  27. 33 0
      yudao-module-crm/yudao-module-crm-api/pom.xml
  28. 4 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java
  29. 61 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java
  30. 16 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
  31. 57 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
  32. 8 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java
  33. 38 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java
  34. 47 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java
  35. 70 0
      yudao-module-crm/yudao-module-crm-biz/pom.xml
  36. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/api/package-info.java
  37. 32 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.http
  38. 107 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
  39. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/package-info.java
  40. 57 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessBaseVO.java
  41. 16 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessCreateReqVO.java
  42. 75 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExcelVO.java
  43. 74 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessExportReqVO.java
  44. 18 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessPageReqVO.java
  45. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessRespVO.java
  46. 32 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessTransferReqVO.java
  47. 22 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/CrmBusinessUpdateReqVO.java
  48. 119 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/CrmBusinessStatusController.java
  49. 33 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java
  50. 14 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusCreateReqVO.java
  51. 30 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExcelVO.java
  52. 23 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java
  53. 18 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusPageReqVO.java
  54. 15 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java
  55. 20 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java
  56. 110 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/CrmBusinessStatusTypeController.java
  57. 27 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeBaseVO.java
  58. 15 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java
  59. 32 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java
  60. 29 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java
  61. 21 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypePageReqVO.java
  62. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeRespVO.java
  63. 21 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java
  64. 89 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/CrmClueController.java
  65. 4 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/package-info.java
  66. 52 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueBaseVO.java
  67. 14 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueCreateReqVO.java
  68. 66 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExcelVO.java
  69. 52 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueExportReqVO.java
  70. 24 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmCluePageReqVO.java
  71. 27 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
  72. 20 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueUpdateReqVO.java
  73. 135 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java
  74. 73 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactBaseVO.java
  75. 14 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactCreateReqVO.java
  76. 72 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExcelVO.java
  77. 71 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactExportReqVO.java
  78. 79 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactPageReqVO.java
  79. 27 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactRespVO.java
  80. 17 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactSimpleRespVO.java
  81. 20 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/ContactUpdateReqVO.java
  82. 32 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactTransferReqVO.java
  83. 98 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/ContractController.java
  84. 82 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractBaseVO.java
  85. 14 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractCreateReqVO.java
  86. 70 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExcelVO.java
  87. 37 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractExportReqVO.java
  88. 42 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractPageReqVO.java
  89. 22 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractRespVO.java
  90. 20 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/ContractUpdateReqVO.java
  91. 32 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/CrmContractTransferReqVO.java
  92. 210 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
  93. 98 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
  94. 46 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
  95. 80 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerBaseVO.java
  96. 20 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerCreateReqVO.java
  97. 93 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExcelVO.java
  98. 17 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerExportReqVO.java
  99. 34 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigBaseVO.java
  100. 14 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLimitConfigCreateReqVO.java

+ 1 - 0
pom.xml

@@ -21,6 +21,7 @@
 <!--        <module>yudao-module-mp</module>-->
 <!--        <module>yudao-module-pay</module>-->
 <!--        <module>yudao-module-mall</module>-->
+<!--        <module>yudao-module-crm</module>-->
         <!-- 示例项目 -->
 <!--        <module>yudao-example</module>-->
     </modules>

+ 1 - 0
sql/mysql/crm.sql

@@ -0,0 +1 @@
+SET NAMES utf8mb4;

+ 20 - 0
sql/mysql/crm_data.sql

@@ -0,0 +1,20 @@
+
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (184, '回款管理审批状态', 'crm_receivable_check_status', 0, '回款管理审批状态(0 未审核 1 审核通过 2 审核拒绝 3 审核中 4 已撤回)', '1', '2023-10-18 21:44:24', '1', '2023-10-18 21:44:24', b'0', '1970-01-01 00:00:00');
+
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (185, '回款管理-回款方式', 'crm_return_type', 0, '回款管理-回款方式', '1', '2023-10-18 21:54:10', '1', '2023-10-18 21:54:10', b'0', '1970-01-01 00:00:00');
+
+
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1389, 0, '未审核', '0', 'crm_receivable_check_status', 0, 'default', '', '0 未审核 ', '1', '2023-10-18 21:46:00', '1', '2023-10-18 21:47:16', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1390, 1, '审核通过', '1', 'crm_receivable_check_status', 0, 'default', '', '1 审核通过', '1', '2023-10-18 21:46:18', '1', '2023-10-18 21:47:08', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1391, 2, '审核拒绝', '2', 'crm_receivable_check_status', 0, 'default', '', ' 2 审核拒绝', '1', '2023-10-18 21:46:58', '1', '2023-10-18 21:47:21', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1392, 3, '审核中', '3', 'crm_receivable_check_status', 0, 'default', '', ' 3 审核中', '1', '2023-10-18 21:47:35', '1', '2023-10-18 21:47:35', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1393, 4, '已撤回', '4', 'crm_receivable_check_status', 0, 'default', '', ' 4 已撤回', '1', '2023-10-18 21:47:46', '1', '2023-10-18 21:47:46', b'0');
+
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1394, 1, '支票', '1', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:29', '1', '2023-10-18 21:54:29', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1395, 2, '现金', '2', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:41', '1', '2023-10-18 21:54:41', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1396, 3, '邮政汇款', '3', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:54:53', '1', '2023-10-18 21:54:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1397, 4, '电汇', '4', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:07', '1', '2023-10-18 21:55:07', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1398, 5, '网上转账', '5', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:24', '1', '2023-10-18 21:55:24', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1399, 6, '支付宝', '6', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:38', '1', '2023-10-18 21:55:38', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1400, 7, '微信支付', '7', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:55:53', '1', '2023-10-18 21:55:53', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1401, 8, '其他', '8', 'crm_return_type', 0, 'default', '', '', '1', '2023-10-18 21:56:06', '1', '2023-10-18 21:56:06', b'0');

+ 88 - 0
sql/mysql/crm_menu.sql

@@ -0,0 +1,88 @@
+-- ----------------------------
+-- 客户公海配置
+-- ----------------------------
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+   '客户公海配置', '', 2, 0, 2397,
+   'customer-pool-config', 'ep:data-analysis', 'crm/customerPoolConf/index', 0, 'CustomerPoolConf'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户公海配置保存', 'crm:customer-pool-config:update', 3, 1, @parentId,
+   '', '', '', 0
+);
+
+
+
+
+-- ----------------------------
+-- 客户限制配置管理
+-- ----------------------------
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+   '客户限制配置管理', '', 2, 0, 2397,
+   'customer-limit-config', '', 'crm/customerLimitConfig/index', 0, 'CrmCustomerLimitConfig'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置查询', 'crm:customer-limit-config:query', 3, 1, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置创建', 'crm:customer-limit-config:create', 3, 2, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置更新', 'crm:customer-limit-config:update', 3, 3, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置删除', 'crm:customer-limit-config:delete', 3, 4, @parentId,
+   '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+   '客户限制配置导出', 'crm:customer-limit-config:export', 3, 5, @parentId,
+   '', '', '', 0
+);

+ 8 - 0
sql/mysql/pay_wallet.sql

@@ -246,3 +246,11 @@ VALUES (
            '转账订单', '', 2, 3, 1117,
            'transfer', 'ep:credit-card', 'pay/transfer/index', 0, 'PayTransfer'
        );
+
+-- 转账通知脚本
+
+ALTER TABLE `pay_app`
+    ADD COLUMN `transfer_notify_url` varchar(1024) NOT NULL COMMENT '转账结果的回调地址' AFTER `refund_notify_url`;
+ALTER TABLE  `pay_notify_task`
+    MODIFY COLUMN `merchant_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '商户订单编号' AFTER `status`,
+    ADD COLUMN `merchant_transfer_id` varchar(64) COMMENT '商户转账单编号' AFTER `merchant_order_id`;

+ 3 - 3
sql/mysql/ruoyi-vue-pro.sql

@@ -535,8 +535,8 @@ CREATE TABLE `infra_demo01_contact`  (
   `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
   `sex` tinyint(1) NOT NULL COMMENT '性别',
   `birthday` datetime NOT NULL COMMENT '出生年',
-  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
-  `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
+  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '简介',
+  `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
@@ -651,7 +651,7 @@ CREATE TABLE `infra_demo03_student`  (
   `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
   `sex` tinyint NOT NULL COMMENT '性别',
   `birthday` datetime NOT NULL COMMENT '出生日期',
-  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
+  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '简介',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',

+ 4 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java

@@ -35,9 +35,12 @@ public class ServiceErrorCodeRange {
     // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
     // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
     // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
-    // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
     // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
+
+    // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
     // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
     // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
 
+    // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
+
 }

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

@@ -280,6 +280,15 @@ public class CollectionUtils {
         return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
     }
 
+    public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from,
+                                                         Function<? super T, ? extends U> mapper,
+                                                         Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new ArrayList<>();
+        }
+        return from.stream().map(mapper).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
     public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from,
                                                     Function<T, ? extends Stream<? extends U>> func) {
         if (CollUtil.isEmpty(from)) {
@@ -288,4 +297,13 @@ public class CollectionUtils {
         return from.stream().flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
     }
 
+    public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from,
+                                                       Function<? super T, ? extends U> mapper,
+                                                       Function<U, ? extends Stream<? extends R>> func) {
+        if (CollUtil.isEmpty(from)) {
+            return new HashSet<>();
+        }
+        return from.stream().map(mapper).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
 }

+ 28 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = TelephoneValidator.class
+)
+public @interface Telephone {
+
+    String message() default "电话格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 25 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.PhoneUtil;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
+
+    @Override
+    public void initialize(Telephone annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (CharSequenceUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value);
+    }
+
+}

+ 9 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 
 import java.util.Map;
 
@@ -86,4 +87,12 @@ public interface PayClient {
      */
     PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
 
+    /**
+     * 获得转账订单信息
+     *
+     * @param outTradeNo 外部订单号
+     * @param type 转账类型
+     * @return 转账信息
+     */
+    PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
 }

+ 15 - 2
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java

@@ -53,11 +53,24 @@ public class PayTransferRespDTO {
     /**
      * 创建【WAITING】状态的转账返回
      */
-    public static PayTransferRespDTO waitingOf(String channelOrderNo,
+    public static PayTransferRespDTO waitingOf(String channelTransferNo,
                                              String outTransferNo, Object rawData) {
         PayTransferRespDTO respDTO = new PayTransferRespDTO();
         respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
-        respDTO.channelTransferNo = channelOrderNo;
+        respDTO.channelTransferNo = channelTransferNo;
+        respDTO.outTransferNo = outTransferNo;
+        respDTO.rawData = rawData;
+        return respDTO;
+    }
+
+    /**
+     * 创建【IN_PROGRESS】状态的转账返回
+     */
+    public static PayTransferRespDTO dealingOf(String channelTransferNo,
+                                               String outTransferNo, Object rawData) {
+        PayTransferRespDTO respDTO = new PayTransferRespDTO();
+        respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
+        respDTO.channelTransferNo = channelTransferNo;
         respDTO.outTransferNo = outTransferNo;
         respDTO.rawData = rawData;
         return respDTO;

+ 19 - 3
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java

@@ -188,11 +188,11 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 
     @Override
     public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
+        validatePayTransferReqDTO(reqDTO);
         PayTransferRespDTO resp;
-        try{
-            validatePayTransferReqDTO(reqDTO);
+        try {
             resp = doUnifiedTransfer(reqDTO);
-        }catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
             throw ex;
         } catch (Throwable ex) {
             // 系统异常,则包装成 PayException 异常抛出
@@ -219,9 +219,25 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         }
     }
 
+    @Override
+    public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        try {
+            return doGetTransfer(outTradeNo, type);
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+            throw ex;
+        } catch (Throwable ex) {
+            log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
+                    getId(), outTradeNo, type, ex);
+            throw buildPayException(ex);
+        }
+    }
+
     protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
             throws Throwable;
 
+    protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
+            throws Throwable;
+
     // ========== 各种工具方法 ==========
 
     private PayException buildPayException(Throwable ex) {

+ 77 - 31
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java

@@ -23,14 +23,8 @@ import com.alipay.api.AlipayResponse;
 import com.alipay.api.DefaultAlipayClient;
 import com.alipay.api.domain.*;
 import com.alipay.api.internal.util.AlipaySignature;
-import com.alipay.api.request.AlipayFundTransUniTransferRequest;
-import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
-import com.alipay.api.request.AlipayTradeQueryRequest;
-import com.alipay.api.request.AlipayTradeRefundRequest;
-import com.alipay.api.response.AlipayFundTransUniTransferResponse;
-import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
-import com.alipay.api.response.AlipayTradeQueryResponse;
-import com.alipay.api.response.AlipayTradeRefundResponse;
+import com.alipay.api.request.*;
+import com.alipay.api.response.*;
 import lombok.Getter;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -126,7 +120,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         }
         // 2.2 解析订单的状态
         Integer status = parseStatus(response.getTradeStatus());
-        Assert.notNull(status,  () -> {
+        Assert.notNull(status, () -> {
             throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
         });
         return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
@@ -228,7 +222,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
         // 1.1 校验公钥类型 必须使用公钥证书模式
         if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
-            throw exception0(ERROR_CONFIGURATION.getCode(),"支付宝单笔转账必须使用公钥证书模式");
+            throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
         }
         // 1.2 构建 AlipayFundTransUniTransferModel
         AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
@@ -238,44 +232,96 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         model.setOutBizNo(reqDTO.getOutTransferNo());
         model.setProductCode("TRANS_ACCOUNT_NO_PWD");    // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
         model.setBizScene("DIRECT_TRANSFER");           // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
-        model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+        if (reqDTO.getChannelExtras() != null) {
+            model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+        }
+        // ② 个性化的参数
+        Participant payeeInfo = new Participant();
         PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
         switch (transferType) {
             // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
             // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
             case ALIPAY_BALANCE: {
-                // ② 个性化的参数
-                Participant payeeInfo = new Participant();
                 payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
                 payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
                 payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
                 model.setPayeeInfo(payeeInfo);
-                // 1.3 构建 AlipayFundTransUniTransferRequest
-                AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
-                request.setBizModel(model);
-                // 执行请求
-                AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
-                // 处理结果
-                if (!response.isSuccess()) {
-                    // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
-                    if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
-                        return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
-                    }
-                    return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
-                            reqDTO.getOutTransferNo(), response);
-                }
-                return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
-                        response.getOutBizNo(), response);
+                break;
             }
             case BANK_CARD: {
-                Participant payeeInfo = new Participant();
                 payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
                 // TODO 待实现
                 throw exception(NOT_IMPLEMENTED);
             }
             default: {
-                throw exception0(BAD_REQUEST.getCode(),"不正确的转账类型: {}",transferType);
+                throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
+            }
+        }
+        // 1.3 构建 AlipayFundTransUniTransferRequest
+        AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
+        request.setBizModel(model);
+        // 执行请求
+        AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
+        // 处理结果
+        if (!response.isSuccess()) {
+            // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
+            // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
+            if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+                return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
+            }
+            return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    reqDTO.getOutTransferNo(), response);
+        } else {
+            if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+                return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                        reqDTO.getOutTransferNo(), response);
+            }
+            if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING"  处理中
+                return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
+            }
+            return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
+                    response.getOutBizNo(), response);
+        }
+
+    }
+
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable {
+        // 1.1 构建 AlipayFundTransCommonQueryModel
+        AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
+        model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
+        model.setBizScene("DIRECT_TRANSFER"); //业务场景
+        model.setOutBizNo(outTradeNo);
+        // 1.2 构建 AlipayFundTransCommonQueryRequest
+        AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
+        request.setBizModel(model);
+
+        // 2.1 执行请求
+        AlipayFundTransCommonQueryResponse response;
+        if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
+            response = client.certificateExecute(request);
+        } else {
+            response = client.execute(request);
+        }
+        // 2.2 处理返回结果
+        if (response.isSuccess()) {
+            if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+                return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                        outTradeNo, response);
+            }
+            if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
+                return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
             }
+            return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
+                    response.getOutBizNo(), response);
+        } else {
+            // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+            // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+            if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+                return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
+            }
+            return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    outTradeNo, response);
         }
     }
 

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifie
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 
 import java.time.LocalDateTime;
 import java.util.Map;
@@ -71,4 +72,9 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
         throw new UnsupportedOperationException("待实现");
     }
 
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
 }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java

@@ -16,6 +16,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
@@ -431,6 +432,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
        throw new UnsupportedOperationException("待实现");
     }
+
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
     // ========== 各种工具方法 ==========
 
     static String formatDateV2(LocalDateTime time) {

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java

@@ -38,4 +38,8 @@ public enum PayTransferStatusRespEnum {
     public static boolean isClosed(Integer status) {
         return Objects.equals(status, CLOSED.getStatus());
     }
+
+    public static boolean isInProgress(Integer status) {
+        return Objects.equals(status, IN_PROGRESS.getStatus());
+    }
 }

+ 40 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.framework.flowable.core.context;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工作流--用户用到的上下文相关信息
+ */
+public class FlowableContextHolder {
+
+    private static final ThreadLocal<Map<String, List<Long>>> ASSIGNEE = new TransmittableThreadLocal<>();
+
+    /**
+     * 通过流程任务的定义 key ,拿到提前选好的审批人
+     * 此方法目的:首次创建流程实例时,数据库中还查询不到 assignee 字段,所以存入上下文中获取
+     *
+     * @param taskDefinitionKey 流程任务 key
+     * @return 审批人 ID 集合
+     */
+    public static List<Long> getAssigneeByTaskDefinitionKey(String taskDefinitionKey) {
+        if (CollUtil.isNotEmpty(ASSIGNEE.get())) {
+            return ASSIGNEE.get().get(taskDefinitionKey);
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * 存入提前选好的审批人到上下文线程变量中
+     *
+     * @param assignee 流程任务 key -> 审批人 ID 炅和
+     */
+    public static void setAssignee(Map<String, List<Long>> assignee) {
+        ASSIGNEE.set(assignee);
+    }
+
+}

+ 8 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java

@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.baomidou.mybatisplus.extension.toolkit.Db;
 import com.github.yulichang.base.MPJBaseMapper;
+import com.github.yulichang.interfaces.MPJBaseJoin;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.Collection;
@@ -39,6 +40,13 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
     }
 
+    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
+        IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
+        selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
+        // 转换返回
+        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+    }
+
     default T selectOne(String field, Object value) {
         return selectOne(new QueryWrapper<T>().eq(field, value));
     }

+ 12 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.api.task.dto;
 import lombok.Data;
 
 import javax.validation.constraints.NotEmpty;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -30,4 +31,15 @@ public class BpmProcessInstanceCreateReqDTO {
      */
     @NotEmpty(message = "业务的唯一标识")
     private String businessKey;
+
+    // TODO @hai:assignees 复数
+    /**
+     * 提前指派的审批人
+     *
+     * key:taskKey 任务编码
+     * value:审批人的数组
+     * 例如: { taskKey1 :[1, 2] },则表示 taskKey1 这个任务,提前设定了,由 userId 为 1,2 的用户进行审批
+     */
+    private Map<String, List<Long>> assignee;
+
 }

+ 5 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import javax.validation.constraints.NotEmpty;
+import java.util.List;
 import java.util.Map;
 
 @Schema(description = "管理后台 - 流程实例的创建 Request VO")
@@ -17,4 +18,8 @@ public class BpmProcessInstanceCreateReqVO {
     @Schema(description = "变量实例")
     private Map<String, Object> variables;
 
+    // TODO @hai:assignees 复数
+    @Schema(description = "提前指派的审批人", requiredMode = Schema.RequiredMode.REQUIRED, example = "{taskKey1: [1, 2]}")
+    private Map<String, List<Long>> assignee;
+
 }

+ 8 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java

@@ -12,6 +12,7 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 import java.time.LocalDateTime;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -87,4 +88,11 @@ public class BpmProcessInstanceExtDO extends BaseDO {
     @TableField(typeHandler = JacksonTypeHandler.class)
     private Map<String, Object> formVariables;
 
+    // TODO @hai:assignees 复数
+    /**
+     * 提前设定好的审批人
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class, exist = false) // TODO 芋艿:临时 exist = false,避免 db 报错;
+    private Map<String, List<Long>> assignee;
+
 }

+ 13 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java

@@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmTaskAssignRuleMapper;
 import cn.iocoder.yudao.module.bpm.enums.DictTypeConstants;
 import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.PostApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -39,6 +40,7 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import javax.validation.Valid;
 import java.util.*;
+import java.util.function.Function;
 
 import static cn.hutool.core.text.CharSequenceUtil.format;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -77,6 +79,9 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
     private DictDataApi dictDataApi;
     @Resource
     private PermissionApi permissionApi;
+    @Resource
+    @Lazy // 解决循环依赖
+    private BpmProcessInstanceService processInstanceService;
     /**
      * 任务分配脚本
      */
@@ -234,6 +239,14 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
     @Override
     @DataPermission(enable = false) // 忽略数据权限,不然分配会存在问题
     public Set<Long> calculateTaskCandidateUsers(DelegateExecution execution) {
+        // 1. 先从提前选好的审批人中获取
+        List<Long> assignee = processInstanceService.getAssigneeByProcessInstanceIdAndTaskDefinitionKey(
+                execution.getProcessInstanceId(), execution.getCurrentActivityId());
+        if (CollUtil.isNotEmpty(assignee)) {
+            // TODO @hai:new HashSet 即可
+            return convertSet(assignee, Function.identity());
+        }
+        // 2. 通过分配规则,计算审批人
         BpmTaskAssignRuleDO rule = getTaskRule(execution);
         return calculateTaskCandidateUsers(execution, rule);
     }

+ 16 - 5
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java

@@ -49,16 +49,17 @@ public interface BpmProcessInstanceService {
     /**
      * 获得流程实例的分页
      *
-     * @param userId 用户编号
+     * @param userId    用户编号
      * @param pageReqVO 分页请求
      * @return 流程实例的分页
      */
     PageResult<BpmProcessInstancePageItemRespVO> getMyProcessInstancePage(Long userId,
                                                                           @Valid BpmProcessInstanceMyPageReqVO pageReqVO);
+
     /**
      * 创建流程实例(提供给前端)
      *
-     * @param userId 用户编号
+     * @param userId      用户编号
      * @param createReqVO 创建信息
      * @return 实例的编号
      */
@@ -67,7 +68,7 @@ public interface BpmProcessInstanceService {
     /**
      * 创建流程实例(提供给内部)
      *
-     * @param userId 用户编号
+     * @param userId       用户编号
      * @param createReqDTO 创建信息
      * @return 实例的编号
      */
@@ -84,7 +85,7 @@ public interface BpmProcessInstanceService {
     /**
      * 取消流程实例
      *
-     * @param userId 用户编号
+     * @param userId      用户编号
      * @param cancelReqVO 取消信息
      */
     void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO);
@@ -139,9 +140,19 @@ public interface BpmProcessInstanceService {
     /**
      * 更新 ProcessInstance 拓展记录为不通过
      *
-     * @param id 流程编号
+     * @param id     流程编号
      * @param reason 理由。例如说,审批不通过时,需要传递该值
      */
     void updateProcessInstanceExtReject(String id, String reason);
 
+    // TODO @hai:改成 getProcessInstanceAssigneesByTaskDefinitionKey(String id, String taskDefinitionKey)
+    /**
+     * 获取流程实例中,取出指定流程任务提前指定的审批人
+     *
+     * @param processInstanceId 流程实例的编号
+     * @param taskDefinitionKey 流程任务定义的 key
+     * @return 审批人集合
+     */
+    List<Long> getAssigneeByProcessInstanceIdAndTaskDefinitionKey(String processInstanceId, String taskDefinitionKey);
+
 }

文件差异内容过多而无法显示
+ 0 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java


+ 25 - 0
yudao-module-crm/pom.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modules>
+        <module>yudao-module-crm-api</module>
+        <module>yudao-module-crm-biz</module>
+    </modules>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-crm</artifactId>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+
+    <description>
+        crm 包下,客户关系管理(Customer Relationship Management)。
+        例如说:客户、联系人、商机、合同、回款等等
+    </description>
+
+</project>

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

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-crm</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-crm-api</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        crm 模块 API,暴露给其它模块调用
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <!-- 参数校验 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+
+</project>

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

@@ -0,0 +1,4 @@
+/**
+ * crm API 包,定义暴露给其它模块的 API
+ */
+package cn.iocoder.yudao.module.crm.api;

+ 61 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/AuditStatusEnum.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+
+import java.util.Arrays;
+
+// TODO @liuhongfeng:这个状态,还是搞成专属 CrmReceivableDO 专属的 status;
+/**
+ * 流程审批状态枚举类
+ * 0 未审核 1 审核通过 2 审核拒绝 3 审核中 4 已撤回 TODO @liuhongfeng:这一行可以删除,因为已经有枚举属性了哈;
+ * @author 赤焰
+ */
+// TODO @liuhongfeng:可以使用 @Getter、@AllArgsConstructor 简化 get、构造方法
+public enum AuditStatusEnum implements IntArrayValuable {
+
+    // TODO @liuhongfeng:草稿 0;10 审核中;20 审核通过;30 审核拒绝;40 已撤回;主要是留好间隙,万一每个地方要做点拓展; 然后,枚举字段的顺序调整下,审批中,一定要放两个审批通过、拒绝前面哈;
+    /**
+     * 未审批
+     */
+    AUDIT_NEW(0, "未审批"),
+    /**
+     * 审核通过
+     */
+	AUDIT_FINISH(1, "审核通过"),
+    /**
+     * 审核拒绝
+     */
+	AUDIT_REJECT(2, "审核拒绝"),
+    /**
+     * 审核中
+     */
+    AUDIT_DOING(3, "审核中"),
+	/**
+	 * 已撤回
+	 */
+	AUDIT_RETURN(4, "已撤回");
+
+    // TODO liuhongfeng:value 改成 status;desc 改成 name;
+    private final Integer value;
+    private final String desc;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AuditStatusEnum::getValue).toArray();
+
+    AuditStatusEnum(Integer value, String desc) {
+        this.value = value;
+        this.desc = desc;
+    }
+
+    public Integer getValue() {
+        return value;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+}

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

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+/**
+ * CRM 字典类型的枚举类
+ *
+ * @author 芋道源码
+ */
+public interface DictTypeConstants {
+
+    // ========== CRM 模块 ==========
+    String CRM_CUSTOMER_INDUSTRY = "crm_customer_industry"; // CRM 客户所属行业
+    String CRM_CUSTOMER_LEVEL = "crm_customer_level"; // CRM 客户等级
+    String CRM_CUSTOMER_SOURCE = "crm_customer_source"; // CRM 客户来源
+    String CRM_RECEIVABLE_CHECK_STATUS = "crm_receivable_check_status"; // CRM 审批状态
+
+}

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

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * CRM 错误码枚举类
+ * <p>
+ * crm 系统,使用 1-020-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+    // ========== 合同管理 1-020-000-000 ==========
+    ErrorCode CONTRACT_NOT_EXISTS = new ErrorCode(1_020_000_000, "合同不存在");
+
+    // ========== 线索管理 1-020-001-000 ==========
+    ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
+
+    // ========== 商机管理 1-020-002-000 ==========
+    ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
+
+    // TODO @lilleo:商机状态、商机类型,都单独错误码段
+
+    ErrorCode BUSINESS_STATUS_TYPE_NOT_EXISTS = new ErrorCode(1_020_002_001, "商机状态类型不存在");
+    ErrorCode BUSINESS_STATUS_NOT_EXISTS = new ErrorCode(1_020_002_002, "商机状态不存在");
+
+    // ========== 联系人管理 1-020-003-000 ==========
+    ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
+
+    // ========== 回款管理 1-020-004-000 ==========
+    ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_020_004_000, "回款管理不存在");
+
+    // ========== 合同管理 1-020-005-000 ==========
+    ErrorCode RECEIVABLE_PLAN_NOT_EXISTS = new ErrorCode(1_020_005_000, "回款计划不存在");
+
+    // ========== 客户管理 1_020_006_000 ==========
+    ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_006_000, "客户不存在");
+    ErrorCode CUSTOMER_OWNER_EXISTS = new ErrorCode(1_020_006_001, "客户已存在所属负责人");
+    ErrorCode CUSTOMER_LOCKED = new ErrorCode(1_020_006_002, "客户状态已锁定");
+    ErrorCode CUSTOMER_ALREADY_DEAL = new ErrorCode(1_020_006_003, "客户已交易");
+    // TODO @wanwan:这 2 个单独配置段噢
+    ErrorCode CUSTOMER_POOL_CONFIG_ERROR = new ErrorCode(1_020_006_001, "客户公海规则设置不正确");
+    ErrorCode CUSTOMER_LIMIT_CONFIG_NOT_EXISTS = new ErrorCode(1_020_006_002, "客户限制配置不存在");
+
+    // ========== 权限管理 1_020_007_000 ==========
+    ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
+    ErrorCode CRM_PERMISSION_DENIED = new ErrorCode(1_020_007_001, "{}操作失败,原因:没有权限");
+    ErrorCode CRM_PERMISSION_MODEL_NOT_EXISTS = new ErrorCode(1_020_007_002, "{}不存在");
+    ErrorCode CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS = new ErrorCode(1_020_007_003, "{}操作失败,原因:转移对象已经是该负责人");
+
+    // ========== 产品 1_020_008_000 ==========
+    ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_020_008_000, "产品不存在");
+    ErrorCode PRODUCT_NO_EXISTS = new ErrorCode(1_020_008_001, "产品编号已存在");
+
+    // ========== 产品分类 1_020_009_000 ==========
+    ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_020_009_000, "产品分类不存在");
+
+}

+ 8 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ReturnTypeEnum.java

@@ -0,0 +1,8 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+// TODO @liuhongfeng:这个的作用是?
+/**
+ * @author 赤焰
+ */
+public enum ReturnTypeEnum {
+}

+ 38 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLevelEnum.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.enums.customer;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * CRM 客户等级
+ *
+ * @author Wanwan
+ */
+@Getter
+@AllArgsConstructor
+public enum CrmCustomerLevelEnum implements IntArrayValuable {
+
+    IMPORTANT(1, "A(重点客户)"),
+    GENERAL(2, "B(普通客户)"),
+    LOW_PRIORITY(3, "C(非优先客户)");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLevelEnum::getLevel).toArray();
+
+    /**
+     * 状态
+     */
+    private final Integer level;
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 47 - 0
yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerSceneEnum.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.crm.enums.customer;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+// TODO @puhui999:这个应该是 crm 全局的,不仅仅属于 customer 客户哈;
+/**
+ * CRM 客户等级
+ *
+ * @author Wanwan
+ */
+@Getter
+@AllArgsConstructor
+public enum CrmCustomerSceneEnum implements IntArrayValuable {
+
+    OWNER(1, "我负责的客户"),
+    FOLLOW(2, "我关注的客户");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerSceneEnum::getType).toArray();
+
+    /**
+     * 场景类型
+     */
+    private final Integer type;
+    /**
+     * 场景名称
+     */
+    private final String name;
+
+    public static boolean isOwner(Integer type) {
+        return ObjUtil.equal(OWNER.getType(), type);
+    }
+
+    public static boolean isFollow(Integer type) {
+        return ObjUtil.equal(FOLLOW.getType(), type);
+    }
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 70 - 0
yudao-module-crm/yudao-module-crm-biz/pom.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-crm</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-crm-biz</artifactId>
+
+    <name>${project.artifactId}</name>
+    <description>
+        crm 包下,客户关系管理(Customer Relationship Management)。
+        例如说:客户、联系人、商机、合同、回款等等
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-system-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-crm-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+        </dependency>
+    </dependencies>
+</project>

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

@@ -0,0 +1,4 @@
+/**
+ * crm API 实现类,定义暴露给其它模块的 API
+ */
+package cn.iocoder.yudao.module.crm.api;

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

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

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

@@ -0,0 +1,107 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.*;
+import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - 商机")
+@RestController
+@RequestMapping("/crm/business")
+@Validated
+public class CrmBusinessController {
+
+    @Resource
+    private CrmBusinessService businessService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建商机")
+    @PreAuthorize("@ss.hasPermission('crm:business:create')")
+    public CommonResult<Long> createBusiness(@Valid @RequestBody CrmBusinessCreateReqVO createReqVO) {
+        return success(businessService.createBusiness(createReqVO, getLoginUserId()));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新商机")
+    @PreAuthorize("@ss.hasPermission('crm:business:update')")
+    public CommonResult<Boolean> updateBusiness(@Valid @RequestBody CrmBusinessUpdateReqVO updateReqVO) {
+        businessService.updateBusiness(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除商机")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:business:delete')")
+    public CommonResult<Boolean> deleteBusiness(@RequestParam("id") Long id) {
+        businessService.deleteBusiness(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得商机")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<CrmBusinessRespVO> getBusiness(@RequestParam("id") Long id) {
+        CrmBusinessDO business = businessService.getBusiness(id);
+        return success(CrmBusinessConvert.INSTANCE.convert(business));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商机分页")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPage(@Valid CrmBusinessPageReqVO pageVO) {
+        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO, getLoginUserId());
+        return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/pool-page")
+    @Operation(summary = "获得商机公海分页")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPoolPage(@Valid CrmBusinessPageReqVO pageVO) {
+        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO, CrmPermissionDO.POOL_USER_ID);
+        return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出商机 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:business:export')")
+    @OperateLog(type = EXPORT)
+    public void exportBusinessExcel(@Valid CrmBusinessExportReqVO exportReqVO,
+                                    HttpServletResponse response) throws IOException {
+        List<CrmBusinessDO> list = businessService.getBusinessList(exportReqVO);
+        // 导出 Excel
+        List<CrmBusinessExcelVO> datas = CrmBusinessConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "商机.xls", "数据", CrmBusinessExcelVO.class, datas);
+    }
+
+    @PutMapping("/transfer")
+    @Operation(summary = "商机转移")
+    @PreAuthorize("@ss.hasPermission('crm:business:update')")
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmBusinessTransferReqVO reqVO) {
+        businessService.transferBusiness(reqVO, getLoginUserId());
+        return success(true);
+    }
+
+}

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

@@ -0,0 +1,4 @@
+/**
+ * 商机(销售机会)
+ */
+package cn.iocoder.yudao.module.crm.controller.admin.business;

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

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 商机 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmBusinessBaseVO {
+
+    @Schema(description = "商机名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotNull(message = "商机名称不能为空")
+    private String name;
+
+    @Schema(description = "商机状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714")
+    @NotNull(message = "商机状态类型不能为空")
+    private Long statusTypeId;
+
+    @Schema(description = "商机状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30320")
+    @NotNull(message = "商机状态不能为空")
+    private Long statusId;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactNextTime;
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10299")
+    @NotNull(message = "客户不能为空")
+    private Long customerId;
+
+    @Schema(description = "预计成交日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime dealTime;
+
+    @Schema(description = "商机金额", example = "12371")
+    private Integer price;
+
+    // TODO @ljileo:折扣使用 Integer 类型,存储时,默认 * 100;展示的时候,前端需要 / 100;避免精度丢失问题
+    @Schema(description = "整单折扣")
+    private Integer discountPercent;
+
+    @Schema(description = "产品总金额", example = "12025")
+    private BigDecimal productPrice;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+}

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

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 商机创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessCreateReqVO extends CrmBusinessBaseVO {
+
+    // TODO @ljileo:新建的时候,应该可以传递添加的产品;
+
+}

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

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

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

@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 商机 Excel 导出 Request VO,参数和 CrmBusinessPageReqVO 是一致的")
+@Data
+public class CrmBusinessExportReqVO {
+
+    @Schema(description = "商机名称", example = "李四")
+    private String name;
+
+    @Schema(description = "商机状态类型编号", example = "25714")
+    private Long statusTypeId;
+
+    @Schema(description = "商机状态编号", example = "30320")
+    private Long statusId;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactNextTime;
+
+    @Schema(description = "客户编号", example = "10299")
+    private Long customerId;
+
+    @Schema(description = "预计成交日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] dealTime;
+
+    @Schema(description = "商机金额", example = "12371")
+    private BigDecimal price;
+
+    @Schema(description = "整单折扣")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "产品总金额", example = "12025")
+    private BigDecimal productPrice;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "负责人的用户编号", example = "25562")
+    private Long ownerUserId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "只读权限的用户编号数组")
+    private String roUserIds;
+
+    @Schema(description = "读写权限的用户编号数组")
+    private String rwUserIds;
+
+    @Schema(description = "1赢单2输单3无效", example = "1")
+    private Integer endStatus;
+
+    @Schema(description = "结束时的备注", example = "你说的对")
+    private String endRemark;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactLastTime;
+
+    @Schema(description = "跟进状态", example = "1")
+    private Integer followUpStatus;
+
+}

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

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 商机分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessPageReqVO extends PageParam {
+
+    @Schema(description = "商机名称", example = "李四")
+    private String name;
+
+}

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

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 商机 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessRespVO extends CrmBusinessBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

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

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 商机转移 Request VO")
+@Data
+public class CrmBusinessTransferReqVO {
+
+    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer oldOwnerPermissionLevel;
+
+}

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

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.crm.controller.admin.business.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 商机更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessUpdateReqVO extends CrmBusinessBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    @NotNull(message = "主键不能为空")
+    private Long id;
+
+    // TODO @ljileo:修改的时候,应该可以传递添加的产品;
+
+}

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

@@ -0,0 +1,119 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo.*;
+import cn.iocoder.yudao.module.crm.convert.businessstatus.CrmBusinessStatusConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatus.CrmBusinessStatusDO;
+import cn.iocoder.yudao.module.crm.service.businessstatus.CrmBusinessStatusService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+// TODO @lilleo:这个模块,可以挪到 business 下;这样我打开 business 包下,就知道,噢~原来里面有 business 商机、有 type 状态类型、status 具体状态;
+@Tag(name = "管理后台 - 商机状态")
+@RestController
+@RequestMapping("/crm/business-status")
+@Validated
+public class CrmBusinessStatusController {
+
+    @Resource
+    private CrmBusinessStatusService businessStatusService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建商机状态")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:create')")
+    public CommonResult<Long> createBusinessStatus(@Valid @RequestBody CrmBusinessStatusCreateReqVO createReqVO) {
+        return success(businessStatusService.createBusinessStatus(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新商机状态")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:update')")
+    public CommonResult<Boolean> updateBusinessStatus(@Valid @RequestBody CrmBusinessStatusUpdateReqVO updateReqVO) {
+        businessStatusService.updateBusinessStatus(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除商机状态")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:business-status:delete')")
+    public CommonResult<Boolean> deleteBusinessStatus(@RequestParam("id") Long id) {
+        businessStatusService.deleteBusinessStatus(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得商机状态")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<CrmBusinessStatusRespVO> getBusinessStatus(@RequestParam("id") Long id) {
+        CrmBusinessStatusDO businessStatus = businessStatusService.getBusinessStatus(id);
+        return success(CrmBusinessStatusConvert.INSTANCE.convert(businessStatus));
+    }
+
+    // TODO @lilleo:这个接口,暂时用不到,可以考虑先删除掉
+    @GetMapping("/list")
+    @Operation(summary = "获得商机状态列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusList(@RequestParam("ids") Collection<Long> ids) {
+        List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusList(ids);
+        return success(CrmBusinessStatusConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商机状态分页")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<PageResult<CrmBusinessStatusRespVO>> getBusinessStatusPage(@Valid CrmBusinessStatusPageReqVO pageVO) {
+        PageResult<CrmBusinessStatusDO> pageResult = businessStatusService.getBusinessStatusPage(pageVO);
+        return success(CrmBusinessStatusConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出商机状态 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:export')")
+    @OperateLog(type = EXPORT)
+    public void exportBusinessStatusExcel(@Valid CrmBusinessStatusExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusList(exportReqVO);
+        // 导出 Excel
+        List<CrmBusinessStatusExcelVO> datas = CrmBusinessStatusConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "商机状态.xls", "数据", CrmBusinessStatusExcelVO.class, datas);
+    }
+
+    // TODO 芋艿:后续再看看
+    @GetMapping("/get-simple-list")
+    @Operation(summary = "获得商机状态列表")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusListByTypeId(@RequestParam("typeId") Integer typeId) {
+        List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusListByTypeId(typeId);
+        return success(CrmBusinessStatusConvert.INSTANCE.convertList(list));
+    }
+
+    // TODO 芋艿:后续再看看
+    @GetMapping("/get-all-list")
+    @Operation(summary = "获得商机状态列表")
+    @PreAuthorize("@ss.hasPermission('crm:business-status:query')")
+    public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusList() {
+        List<CrmBusinessStatusDO> list = businessStatusService.getBusinessStatusList();
+        return success(CrmBusinessStatusConvert.INSTANCE.convertList(list));
+    }
+
+}

+ 33 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusBaseVO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 商机状态 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmBusinessStatusBaseVO {
+
+    // TODO @lilleo:example 要写下
+
+    @Schema(description = "状态类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22882")
+    @NotNull(message = "状态类型编号不能为空")
+    private Long typeId;
+
+    @Schema(description = "状态名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotNull(message = "状态名不能为空")
+    private String name;
+
+    // TODO @lilleo:percent 应该是 Integer;
+    @Schema(description = "赢单率")
+    private String percent;
+
+    // TODO @lilleo:这个是不是不用前端新增和修改的时候传递,交给顺序计算出来,存储起来就好了;
+    @Schema(description = "排序")
+    private Integer sort;
+
+}

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

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 商机状态创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusCreateReqVO extends CrmBusinessStatusBaseVO {
+
+}

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

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
+/**
+ * 商机状态 Excel VO
+ *
+ * @author ljlleo
+ */
+@Data
+public class CrmBusinessStatusExcelVO {
+
+    @ExcelProperty("主键")
+    private Long id;
+
+    @ExcelProperty("状态类型编号")
+    private Long typeId;
+
+    @ExcelProperty("状态名")
+    private String name;
+
+    @ExcelProperty("赢单率")
+    private String percent;
+
+    @ExcelProperty("排序")
+    private Integer sort;
+
+}

+ 23 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusExportReqVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
+@Schema(description = "管理后台 - 商机状态 Excel 导出 Request VO,参数和 CrmBusinessStatusPageReqVO 是一致的")
+@Data
+public class CrmBusinessStatusExportReqVO {
+
+    @Schema(description = "状态类型编号", example = "22882")
+    private Long typeId;
+
+    @Schema(description = "状态名", example = "李四")
+    private String name;
+
+    @Schema(description = "赢单率")
+    private String percent;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+}

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

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

+ 15 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusRespVO.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+@Schema(description = "管理后台 - 商机状态 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusRespVO extends CrmBusinessStatusBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6802")
+    private Long id;
+
+}

+ 20 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatus/vo/CrmBusinessStatusUpdateReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 商机状态更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusUpdateReqVO extends CrmBusinessStatusBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6802")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+}

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

@@ -0,0 +1,110 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo.*;
+import cn.iocoder.yudao.module.crm.convert.businessstatustype.CrmBusinessStatusTypeConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.businessstatustype.CrmBusinessStatusTypeDO;
+import cn.iocoder.yudao.module.crm.service.businessstatustype.CrmBusinessStatusTypeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+// TODO @lilleo:这个模块,可以挪到 business 下;这样我打开 business 包下,就知道,噢~原来里面有 business 商机、有 type 状态类型、status 具体状态;
+@Tag(name = "管理后台 - 商机状态类型")
+@RestController
+@RequestMapping("/crm/business-status-type")
+@Validated
+public class CrmBusinessStatusTypeController {
+
+    @Resource
+    private CrmBusinessStatusTypeService businessStatusTypeService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建商机状态类型")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:create')")
+    public CommonResult<Long> createBusinessStatusType(@Valid @RequestBody CrmBusinessStatusTypeCreateReqVO createReqVO) {
+        return success(businessStatusTypeService.createBusinessStatusType(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新商机状态类型")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:update')")
+    public CommonResult<Boolean> updateBusinessStatusType(@Valid @RequestBody CrmBusinessStatusTypeUpdateReqVO updateReqVO) {
+        businessStatusTypeService.updateBusinessStatusType(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除商机状态类型")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:delete')")
+    public CommonResult<Boolean> deleteBusinessStatusType(@RequestParam("id") Long id) {
+        businessStatusTypeService.deleteBusinessStatusType(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得商机状态类型")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
+    public CommonResult<CrmBusinessStatusTypeRespVO> getBusinessStatusType(@RequestParam("id") Long id) {
+        CrmBusinessStatusTypeDO businessStatusType = businessStatusTypeService.getBusinessStatusType(id);
+        return success(CrmBusinessStatusTypeConvert.INSTANCE.convert(businessStatusType));
+    }
+
+    // TODO @lilleo:这个接口,暂时用不到,可以考虑先删除掉
+    @GetMapping("/list")
+    @Operation(summary = "获得商机状态类型列表")
+    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
+    public CommonResult<List<CrmBusinessStatusTypeRespVO>> getBusinessStatusTypeList(@RequestParam("ids") Collection<Long> ids) {
+        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeList(ids);
+        return success(CrmBusinessStatusTypeConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商机状态类型分页")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
+    public CommonResult<PageResult<CrmBusinessStatusTypeRespVO>> getBusinessStatusTypePage(@Valid CrmBusinessStatusTypePageReqVO pageVO) {
+        PageResult<CrmBusinessStatusTypeDO> pageResult = businessStatusTypeService.getBusinessStatusTypePage(pageVO);
+        return success(CrmBusinessStatusTypeConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出商机状态类型 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:export')")
+    @OperateLog(type = EXPORT)
+    public void exportBusinessStatusTypeExcel(@Valid CrmBusinessStatusTypeExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeList(exportReqVO);
+        // 导出 Excel
+        List<CrmBusinessStatusTypeExcelVO> datas = CrmBusinessStatusTypeConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "商机状态类型.xls", "数据", CrmBusinessStatusTypeExcelVO.class, datas);
+    }
+
+    @GetMapping("/get-simple-list")
+    @Operation(summary = "获得商机状态类型列表")
+    @PreAuthorize("@ss.hasPermission('crm:business-status-type:query')")
+    public CommonResult<List<CrmBusinessStatusTypeRespVO>> getBusinessStatusTypeList() {
+        List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(CrmBusinessStatusTypeConvert.INSTANCE.convertList(list));
+    }
+
+}

+ 27 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeBaseVO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 商机状态类型 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmBusinessStatusTypeBaseVO {
+
+    @Schema(description = "状态类型名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    @NotNull(message = "状态类型名不能为空")
+    private String name;
+
+    @Schema(description = "使用的部门编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "使用的部门编号不能为空")
+    private String deptIds;
+
+    @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "开启状态不能为空")
+    private Boolean status;
+
+}

+ 15 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeCreateReqVO.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+// TODO 状态类型和状态添加,是在一个请求里,所以需要把 CrmBusinessStatusCreateReqVO 融合进来;
+@Schema(description = "管理后台 - 商机状态类型创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusTypeCreateReqVO extends CrmBusinessStatusTypeBaseVO {
+
+}

+ 32 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExcelVO.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
+/**
+ * 商机状态类型 Excel VO
+ *
+ * @author ljlleo
+ */
+@Data
+public class CrmBusinessStatusTypeExcelVO {
+
+    @ExcelProperty("主键")
+    private Long id;
+
+    @ExcelProperty("状态类型名")
+    private String name;
+
+    @ExcelProperty("使用的部门编号")
+    private String deptIds;
+
+    @ExcelProperty("开启状态")
+    private Boolean status;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 29 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeExportReqVO.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+// TODO @lilleo:这个暂时不需要;嘿嘿~不是每个模块都需要导出哈
+@Schema(description = "管理后台 - 商机状态类型 Excel 导出 Request VO,参数和 CrmBusinessStatusTypePageReqVO 是一致的")
+@Data
+public class CrmBusinessStatusTypeExportReqVO {
+
+    @Schema(description = "状态类型名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "使用的部门编号")
+    private String deptIds;
+
+    @Schema(description = "开启状态", example = "1")
+    private Boolean status;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 21 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypePageReqVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 商机状态类型分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusTypePageReqVO extends PageParam {
+
+    @Schema(description = "状态类型名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "开启状态", example = "1")
+    private Boolean status;
+
+}

+ 19 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeRespVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 商机状态类型 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusTypeRespVO extends CrmBusinessStatusTypeBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "24019")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 21 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/businessstatustype/vo/CrmBusinessStatusTypeUpdateReqVO.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.crm.controller.admin.businessstatustype.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+// TODO 状态类型和状态添加,是在一个请求里,所以需要把 CrmBusinessStatusUpdateReqVO 融合进来;
+@Schema(description = "管理后台 - 商机状态类型更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmBusinessStatusTypeUpdateReqVO extends CrmBusinessStatusTypeBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "24019")
+    @NotNull(message = "主键不能为空")
+    private Long id;
+
+}

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

@@ -0,0 +1,89 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.*;
+import cn.iocoder.yudao.module.crm.convert.clue.CrmClueConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
+import cn.iocoder.yudao.module.crm.service.clue.CrmClueService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - 线索")
+@RestController
+@RequestMapping("/crm/clue")
+@Validated
+public class CrmClueController {
+
+    @Resource
+    private CrmClueService clueService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建线索")
+    @PreAuthorize("@ss.hasPermission('crm:clue:create')")
+    public CommonResult<Long> createClue(@Valid @RequestBody CrmClueCreateReqVO createReqVO) {
+        return success(clueService.createClue(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新线索")
+    @PreAuthorize("@ss.hasPermission('crm:clue:update')")
+    public CommonResult<Boolean> updateClue(@Valid @RequestBody CrmClueUpdateReqVO updateReqVO) {
+        clueService.updateClue(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除线索")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:clue:delete')")
+    public CommonResult<Boolean> deleteClue(@RequestParam("id") Long id) {
+        clueService.deleteClue(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得线索")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<CrmClueRespVO> getClue(@RequestParam("id") Long id) {
+        CrmClueDO clue = clueService.getClue(id);
+        return success(CrmClueConvert.INSTANCE.convert(clue));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得线索分页")
+    @PreAuthorize("@ss.hasPermission('crm:clue:query')")
+    public CommonResult<PageResult<CrmClueRespVO>> getCluePage(@Valid CrmCluePageReqVO pageVO) {
+        PageResult<CrmClueDO> pageResult = clueService.getCluePage(pageVO);
+        return success(CrmClueConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出线索 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:clue:export')")
+    @OperateLog(type = EXPORT)
+    public void exportClueExcel(@Valid CrmClueExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CrmClueDO> list = clueService.getClueList(exportReqVO);
+        // 导出 Excel
+        List<CrmClueExcelVO> datas = CrmClueConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "线索.xls", "数据", CrmClueExcelVO.class, datas);
+    }
+
+}

+ 4 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 线索
+ */
+package cn.iocoder.yudao.module.crm.controller.admin.clue;

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

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import cn.iocoder.yudao.framework.common.validation.Mobile;
+import cn.iocoder.yudao.framework.common.validation.Telephone;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 线索 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmClueBaseVO {
+
+    @Schema(description = "线索名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "线索xxx")
+    @NotEmpty(message = "线索名称不能为空")
+    private String name;
+
+    @Schema(description = "客户 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "520")
+    @NotNull(message = "客户不能为空")
+    private Long customerId;
+
+    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactNextTime;
+
+    @Schema(description = "电话", example = "18000000000")
+    @Telephone
+    private String telephone;
+
+    @Schema(description = "手机号", example = "18000000000")
+    @Mobile
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+}

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

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 线索创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueCreateReqVO extends CrmClueBaseVO {
+
+}

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

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+/**
+ * 线索 Excel VO
+ *
+ * @author Wanwan
+ */
+@Data
+public class CrmClueExcelVO {
+
+    @ExcelProperty("编号")
+    private Long id;
+
+    @ExcelProperty(value = "转化状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean transformStatus;
+
+    @ExcelProperty(value = "跟进状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean followUpStatus;
+
+    @ExcelProperty("线索名称")
+    private String name;
+
+    // TODO 这里需要导出成客户名称
+    @ExcelProperty("客户id")
+    private Long customerId;
+
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("手机号")
+    private String mobile;
+
+    @ExcelProperty("地址")
+    private String address;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

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

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import java.time.LocalDateTime;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 线索 Excel 导出 Request VO,参数和 CrmCluePageReqVO 是一致的")
+@Data
+public class CrmClueExportReqVO {
+
+    @Schema(description = "转化状态", example = "true")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", example = "true")
+    private Boolean followUpStatus;
+
+    @Schema(description = "线索名称", example = "线索xxx")
+    private String name;
+
+    @Schema(description = "客户id", example = "520")
+    private Long customerId;
+
+    @Schema(description = "下次联系时间", example = "2023-10-18 01:00:00")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactNextTime;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Schema(description = "手机号", example = "18000000000")
+    private String mobile;
+
+    @Schema(description = "地址", example = "北京市海淀区")
+    private String address;
+
+    @Schema(description = "负责人的用户编号", example = "27199")
+    private Long ownerUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] contactLastTime;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

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

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 线索分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCluePageReqVO extends PageParam {
+
+    @Schema(description = "线索名称", example = "线索xxx")
+    private String name;
+
+    @Schema(description = "电话", example = "18000000000")
+    private String telephone;
+
+    @Schema(description = "手机号", example = "18000000000")
+    private String mobile;
+
+}

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

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 线索 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueRespVO extends CrmClueBaseVO {
+
+    @Schema(description = "编号,主键自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "10969")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+    @Schema(description = "转化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean transformStatus;
+
+    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    private Boolean followUpStatus;
+
+}

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

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 线索更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmClueUpdateReqVO extends CrmClueBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10969")
+    @NotNull(message = "编号不能为空")
+    private Long id;
+
+}

+ 135 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/ContactController.java

@@ -0,0 +1,135 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.NumberUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerExportReqVO;
+import cn.iocoder.yudao.module.crm.convert.contact.ContactConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.ContactDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.service.contact.ContactService;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import com.google.common.collect.Lists;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+// TODO @zya:crm 所有的类,dou带 Crm 前缀,因为它的名字太通用了,可能和后续的 erp 之类的冲突
+@Tag(name = "管理后台 - CRM 联系人")
+@RestController
+@RequestMapping("/crm/contact")
+@Validated
+public class ContactController {
+
+    @Resource
+    private ContactService contactService;
+    // TODO @zyna:模块内,注入的变量,不用带 crm 前缀哈
+    @Resource
+    private CrmCustomerService crmCustomerService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建联系人")
+    @PreAuthorize("@ss.hasPermission('crm:contact:create')")
+    public CommonResult<Long> createContact(@Valid @RequestBody ContactCreateReqVO createReqVO) {
+        return success(contactService.createContact(createReqVO, getLoginUserId()));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新联系人")
+    @PreAuthorize("@ss.hasPermission('crm:contact:update')")
+    public CommonResult<Boolean> updateContact(@Valid @RequestBody ContactUpdateReqVO updateReqVO) {
+        contactService.updateContact(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除联系人")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:contact:delete')")
+    public CommonResult<Boolean> deleteContact(@RequestParam("id") Long id) {
+        contactService.deleteContact(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得联系人")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
+    public CommonResult<ContactRespVO> getContact(@RequestParam("id") Long id) {
+        ContactDO contact = contactService.getContact(id);
+        // TODO @zyna:需要考虑 null 的情况;
+        ContactRespVO contactRespVO  = ContactConvert.INSTANCE.convert(contact);
+        // TODO @zyna:可以把数据读完后,convert 统一交给 ContactConvert,让 controller 更简洁;而 convert 专门去做一些转换逻辑
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(CollUtil.removeNull(Lists.newArrayList(
+                NumberUtil.parseLong(contact.getCreator()))));
+        contactRespVO.setCreatorName(Optional.ofNullable(userMap.get(NumberUtil.parseLong(contact.getCreator()))).map(AdminUserRespDTO::getNickname).orElse(null));
+        contactRespVO.setCustomerName(Optional.ofNullable(crmCustomerService.getCustomer(contact.getCustomerId())).map(CrmCustomerDO::getName).orElse(null));
+        return success(contactRespVO);
+    }
+
+    // TODO @zyna:url 使用中划线噢;然后,单词的拼写也要注意呀,AllList 是不是更好呀;
+    @GetMapping("/simpleAlllist")
+    @Operation(summary = "获得联系人列表")
+    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
+    public CommonResult<List<ContactSimpleRespVO>> simpleAlllist() {
+        // TODO @zyna:方法名改成,getContactList;方法命名,要动名词,get 动词;all 可以去掉,因为没条件,自然是全部
+        List<ContactDO> list = contactService.allContactList();
+        return success(ContactConvert.INSTANCE.convertAllList(list));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得联系人分页")
+    @PreAuthorize("@ss.hasPermission('crm:contact:query')")
+    public CommonResult<PageResult<ContactRespVO>> getContactPage(@Valid ContactPageReqVO pageVO) {
+        PageResult<ContactDO> pageData = contactService.getContactPage(pageVO);
+        PageResult<ContactRespVO> pageResult =ContactConvert.INSTANCE.convertPage(pageData);
+        // TODO @zyna:需要考虑 null 的情况;
+        // TODO @zyna:可以把数据读完后,convert 统一交给 ContactConvert,让 controller 更简洁;而 convert 专门去做一些转换逻辑
+        //待接口实现后修改
+        List<CrmCustomerDO> crmCustomerDOList = crmCustomerService.getCustomerList(new CrmCustomerExportReqVO());
+        Map<Long,CrmCustomerDO> crmCustomerDOMap = crmCustomerDOList.stream().collect(Collectors.toMap(CrmCustomerDO::getId,v->v));
+        pageResult.getList().forEach(item -> {
+            item.setCustomerName(Optional.ofNullable(crmCustomerDOMap.get(item.getCustomerId())).map(CrmCustomerDO::getName).orElse(null));
+        });
+        return success(pageResult);
+    }
+
+    // TODO @zyna:可以看下新的导出写法,这里调整下
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出联系人 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:contact:export')")
+    @OperateLog(type = EXPORT)
+    public void exportContactExcel(@Valid ContactExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<ContactDO> list = contactService.getContactList(exportReqVO);
+        // 导出 Excel
+        List<ContactExcelVO> datas = ContactConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "crm联系人.xls", "数据", ContactExcelVO.class, datas);
+    }
+
+}

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

@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+// TODO zyna:参考新的 vo,重新拆分下 VO
+/**
+ * CRM 联系人 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ContactBaseVO {
+
+    // TODO @zyna:example 最好都写下
+    // TODO @zyna:必要的字段校验,例如说 @Mobile,@Emal 等等
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+    private LocalDateTime nextTime;
+
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "电话")
+    private String telephone;
+
+    @Schema(description = "电子邮箱")
+    private String email;
+
+    @Schema(description = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime lastTime;
+
+    @Schema(description = "直属上级", example = "23457")
+    private Long parentId;
+
+    @Schema(description = "姓名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "职位")
+    private String post;
+
+    @Schema(description = "QQ")
+    private Long qq;
+
+    @Schema(description = "微信")
+    private String webchat;
+
+    @Schema(description = "性别")
+    private Integer sex;
+
+    @Schema(description = "是否关键决策人")
+    private Boolean policyMakers;
+
+    @Schema(description = "负责人用户编号", example = "14334")
+    private String ownerUserId;
+
+}

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

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 联系人创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactCreateReqVO extends ContactBaseVO {
+
+}

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

@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+// TODO @zyna:参考新的 VO 结构,把 ContactExcelVO 融合到 ContactRespVO 中
+/**
+ * crm联系人 Excel VO
+ *
+ * @author 芋道源码
+ */
+@Data
+@Deprecated
+public class ContactExcelVO {
+
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime nextTime;
+
+    @ExcelProperty("手机号")
+    private String mobile;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("电子邮箱")
+    private String email;
+
+    @ExcelProperty("客户编号")
+    private Long customerId;
+
+    @ExcelProperty("地址")
+    private String address;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime lastTime;
+
+    @ExcelProperty("主键")
+    private Long id;
+
+    @ExcelProperty("直属上级")
+    private Long parentId;
+
+    @ExcelProperty("姓名")
+    private String name;
+
+    @ExcelProperty("职位")
+    private String post;
+
+    @ExcelProperty("QQ")
+    private Long qq;
+
+    @ExcelProperty("微信")
+    private String webchat;
+
+    @ExcelProperty("性别")
+    private Integer sex;
+
+    @ExcelProperty("是否关键决策人")
+    private Boolean policyMakers;
+
+    @ExcelProperty("负责人用户编号")
+    private String ownerUserId;
+
+}

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

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+// TODO @zyna:参考新的 VO 结构,使用 ContactPageReqVO 查询导出的数据
+@Schema(description = "管理后台 - crm联系人 Excel 导出 Request VO,参数和 ContactPageReqVO 是一致的")
+@Data
+@Deprecated
+public class ContactExportReqVO {
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] nextTime;
+
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "电话")
+    private String telephone;
+
+    @Schema(description = "电子邮箱")
+    private String email;
+
+    @Schema(description = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] lastTime;
+
+    @Schema(description = "直属上级", example = "23457")
+    private Long parentId;
+
+    @Schema(description = "姓名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "职位")
+    private String post;
+
+    @Schema(description = "QQ")
+    private Long qq;
+
+    @Schema(description = "微信")
+    private String webchat;
+
+    @Schema(description = "性别")
+    private Integer sex;
+
+    @Schema(description = "是否关键决策人")
+    private Boolean policyMakers;
+
+    @Schema(description = "负责人用户编号", example = "14334")
+    private String ownerUserId;
+
+}

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

@@ -0,0 +1,79 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - crm联系人分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactPageReqVO extends PageParam {
+
+    // TODO @zyna:筛选条件
+    // ●客户:
+    // ●姓名:
+    // ●手机、电话、座机、QQ、微信、邮箱
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] nextTime;
+
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "电话")
+    private String telephone;
+
+    @Schema(description = "电子邮箱")
+    private String email;
+
+    @Schema(description = "客户编号", example = "10795")
+    private Long customerId;
+
+    @Schema(description = "地址")
+    private String address;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] lastTime;
+
+    @Schema(description = "直属上级", example = "23457")
+    private Long parentId;
+
+    @Schema(description = "姓名", example = "芋艿")
+    private String name;
+
+    @Schema(description = "职位")
+    private String post;
+
+    @Schema(description = "QQ")
+    private Long qq;
+
+    @Schema(description = "微信")
+    private String webchat;
+
+    @Schema(description = "性别")
+    private Integer sex;
+
+    @Schema(description = "是否关键决策人")
+    private Boolean policyMakers;
+
+    @Schema(description = "负责人用户编号", example = "14334")
+    private String ownerUserId;
+
+}

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

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - CRM 联系人 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactRespVO extends ContactBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
+    private Long id;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    // TODO  @zyna:example 最好写下;
+
+    @Schema(description = "创建人")
+    private String creatorName;
+
+    @Schema(description = "客户名字")
+    private String customerName;
+
+}

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

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 联系人 Response VO")
+@Data
+@ToString(callSuper = true)
+public class ContactSimpleRespVO {
+    @Schema(description = "姓名", example = "芋艿") // TODO @zyna:requiredMode = Schema.RequiredMode.REQUIRED;需要空一行;字段的顺序改下,id 在 name 前面,会更干净
+    private String name;
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
+    private Long id;
+
+}

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

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 联系人更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContactUpdateReqVO extends ContactBaseVO {
+
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167")
+    @NotNull(message = "主键不能为空")
+    private Long id;
+
+}

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

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
+
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 联系人转移 Request VO")
+@Data
+public class CrmContactTransferReqVO {
+
+    @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer oldOwnerPermissionLevel;
+
+}

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

@@ -0,0 +1,98 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
+import cn.iocoder.yudao.module.crm.convert.contract.ContractConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contract.ContractDO;
+import cn.iocoder.yudao.module.crm.service.contract.ContractService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - CRM 合同")
+@RestController
+@RequestMapping("/crm/contract")
+@Validated
+public class ContractController {
+
+    @Resource
+    private ContractService contractService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建合同")
+    @PreAuthorize("@ss.hasPermission('crm:contract:create')")
+    public CommonResult<Long> createContract(@Valid @RequestBody ContractCreateReqVO createReqVO) {
+        return success(contractService.createContract(createReqVO, getLoginUserId()));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新合同")
+    @PreAuthorize("@ss.hasPermission('crm:contract:update')")
+    public CommonResult<Boolean> updateContract(@Valid @RequestBody ContractUpdateReqVO updateReqVO) {
+        contractService.updateContract(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除合同")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:contract:delete')")
+    public CommonResult<Boolean> deleteContract(@RequestParam("id") Long id) {
+        contractService.deleteContract(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得合同")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
+    public CommonResult<ContractRespVO> getContract(@RequestParam("id") Long id) {
+        ContractDO contract = contractService.getContract(id);
+        return success(ContractConvert.INSTANCE.convert(contract));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得合同分页")
+    @PreAuthorize("@ss.hasPermission('crm:contract:query')")
+    public CommonResult<PageResult<ContractRespVO>> getContractPage(@Valid ContractPageReqVO pageVO) {
+        PageResult<ContractDO> pageResult = contractService.getContractPage(pageVO);
+        return success(ContractConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出合同 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:contract:export')")
+    @OperateLog(type = EXPORT)
+    public void exportContractExcel(@Valid ContractExportReqVO exportReqVO,
+                                    HttpServletResponse response) throws IOException {
+        List<ContractDO> list = contractService.getContractList(exportReqVO);
+        // 导出 Excel
+        List<ContractExcelVO> datas = ContractConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "合同.xls", "数据", ContractExcelVO.class, datas);
+    }
+
+    @PutMapping("/transfer")
+    @Operation(summary = "合同转移")
+    @PreAuthorize("@ss.hasPermission('crm:contract:update')")
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmContractTransferReqVO reqVO) {
+        contractService.transferContract(reqVO, getLoginUserId());
+        return success(true);
+    }
+
+}

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

@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+// TODO @dhb52:所有类,带下 Crm 前缀,避免和别的模块重复
+/**
+ * 合同 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class ContractBaseVO {
+
+    // TODO @dhb52:类似 no 字段的 example 要写xia 哈;
+
+    @Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
+    @NotNull(message = "合同名称不能为空")
+    private String name;
+
+    // TODO @dhb52:这个必须传递
+    @Schema(description = "客户编号", example = "18336")
+    private Long customerId;
+
+    @Schema(description = "商机编号", example = "10864")
+    private Long businessId;
+
+    @Schema(description = "工作流编号", example = "1043")
+    private Long processInstanceId;
+
+    // TODO @dhb52:这个必须传递
+    @Schema(description = "下单日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime orderDate;
+
+    // TODO @dhb52:这个必须传递
+    @Schema(description = "负责人的用户编号", example = "17144")
+    private Long ownerUserId;
+
+    // TODO @芋艿:未来应该支持自动生成;
+    // TODO @dhb52:这个必须传递;
+    @Schema(description = "合同编号")
+    private String no;
+
+    @Schema(description = "开始时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime startTime;
+
+    @Schema(description = "结束时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime endTime;
+
+    @Schema(description = "合同金额", example = "5617")
+    private Integer price;
+
+    @Schema(description = "整单折扣")
+    private Integer discountPercent;
+
+    @Schema(description = "产品总金额", example = "19510")
+    private Integer productPrice;
+
+    @Schema(description = "联系人编号", example = "18546")
+    private Long contactId;
+
+    @Schema(description = "公司签约人", example = "14036")
+    private Long signUserId;
+
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    // TODO @dhb52:增加一个 status 字段:具体有哪些值,你来枚举下;主要页面上有个【草稿】【提交审核】的流程,可以看看。然后要对接工作流,这块也可以看看,不确定的地方问我。
+
+}

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

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - CRM 合同创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContractCreateReqVO extends ContractBaseVO {
+
+}

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

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * CRM 合同 Excel VO
+ *
+ * @author dhb52
+ */
+@Data
+public class ContractExcelVO {
+
+    @ExcelProperty("合同编号")
+    private Long id;
+
+    @ExcelProperty("合同名称")
+    private String name;
+
+    @ExcelProperty("客户编号")
+    private Long customerId;
+
+    @ExcelProperty("商机编号")
+    private Long businessId;
+
+    @ExcelProperty("工作流编号")
+    private Long processInstanceId;
+
+    @ExcelProperty("下单日期")
+    private LocalDateTime orderDate;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("合同编号")
+    private String no;
+
+    @ExcelProperty("开始时间")
+    private LocalDateTime startTime;
+
+    @ExcelProperty("结束时间")
+    private LocalDateTime endTime;
+
+    @ExcelProperty("合同金额")
+    private Integer price;
+
+    @ExcelProperty("整单折扣")
+    private Integer discountPercent;
+
+    @ExcelProperty("产品总金额")
+    private Integer productPrice;
+
+    @ExcelProperty("联系人编号")
+    private Long contactId;
+
+    @ExcelProperty("公司签约人")
+    private Long signUserId;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

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

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - CRM 合同 Excel 导出 Request VO,参数和 ContractPageReqVO 是一致的")
+@Data
+public class ContractExportReqVO {
+
+    @Schema(description = "合同名称", example = "王五")
+    private String name;
+
+    @Schema(description = "客户编号", example = "18336")
+    private Long customerId;
+
+    @Schema(description = "商机编号", example = "10864")
+    private Long businessId;
+
+    @Schema(description = "下单日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] orderDate;
+
+    @Schema(description = "合同编号")
+    private String no;
+
+    @Schema(description = "整单折扣")
+    private Integer discountPercent;
+
+    @Schema(description = "产品总金额", example = "19510")
+    private Integer productPrice;
+
+}

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

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - CRM 合同分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContractPageReqVO extends PageParam {
+
+    @Schema(description = "合同名称", example = "王五")
+    private String name;
+
+    @Schema(description = "客户编号", example = "18336")
+    private Long customerId;
+
+    @Schema(description = "商机编号", example = "10864")
+    private Long businessId;
+
+    @Schema(description = "下单日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] orderDate;
+
+    @Schema(description = "合同编号")
+    private String no;
+
+    @Schema(description = "整单折扣")
+    private Integer discountPercent;
+
+    @Schema(description = "产品总金额", example = "19510")
+    private Integer productPrice;
+
+}

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

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - CRM 合同 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContractRespVO extends ContractBaseVO {
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    private Long id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

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

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 合同更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ContractUpdateReqVO extends ContractBaseVO {
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "合同编号不能为空")
+    private Long id;
+
+}

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

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.crm.controller.admin.contract.vo;
+
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 合同转移 Request VO")
+@Data
+public class CrmContractTransferReqVO {
+
+    @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "联系人编号不能为空")
+    private Long id;
+
+    /**
+     * 新负责人的用户编号
+     */
+    @Schema(description = "新负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10430")
+    @NotNull(message = "新负责人的用户编号不能为空")
+    private Long newOwnerUserId;
+
+    /**
+     * 老负责人加入团队后的权限级别。如果 null 说明移除
+     *
+     * 关联 {@link CrmPermissionLevelEnum}
+     */
+    @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer oldOwnerPermissionLevel;
+
+}

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

@@ -0,0 +1,210 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
+import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmBizTypeEnum;
+import cn.iocoder.yudao.module.crm.framework.enums.CrmPermissionLevelEnum;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.CollectionUtils;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - CRM 客户")
+@RestController
+@RequestMapping("/crm/customer")
+@Validated
+public class CrmCustomerController {
+
+    @Resource
+    private CrmCustomerService customerService;
+
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private CrmPermissionService permissionService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建客户")
+    @PreAuthorize("@ss.hasPermission('crm:customer:create')")
+    public CommonResult<Long> createCustomer(@Valid @RequestBody CrmCustomerCreateReqVO createReqVO) {
+        return success(customerService.createCustomer(createReqVO, getLoginUserId()));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新客户")
+    @PreAuthorize("@ss.hasPermission('crm:customer:update')")
+    public CommonResult<Boolean> updateCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
+        customerService.updateCustomer(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除客户")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:customer:delete')")
+    public CommonResult<Boolean> deleteCustomer(@RequestParam("id") Long id) {
+        customerService.deleteCustomer(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得客户")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<CrmCustomerRespVO> getCustomer(@RequestParam("id") Long id) {
+        // 1. 获取客户
+        CrmCustomerDO customer = customerService.getCustomer(id);
+        if (customer == null) {
+            return success(null);
+        }
+
+        // 2. 拼接数据
+        // 2.1 获取负责人
+        List<CrmPermissionDO> ownerList = permissionService.getPermissionByBizTypeAndBizIdsAndLevel(
+                CrmBizTypeEnum.CRM_CUSTOMER.getType(), Collections.singletonList(customer.getId()),
+                CrmPermissionLevelEnum.OWNER.getLevel());
+        Map<Long, CrmPermissionDO> ownerMap = convertMap(ownerList, CrmPermissionDO::getBizId);
+        // 2.2 获取负责人详情
+        Set<Long> userIds = convertSet(ownerList, CrmPermissionDO::getUserId);
+        userIds.add(Long.parseLong(customer.getCreator())); // 加入创建者
+        List<AdminUserRespDTO> userList = adminUserApi.getUserList(userIds);
+        Map<Long, AdminUserRespDTO> userMap = convertMap(userList, AdminUserRespDTO::getId);
+        // 2.3 获取部门详情
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userList, AdminUserRespDTO::getDeptId));
+        return success(CrmCustomerConvert.INSTANCE.convert(customer, ownerMap, userMap, deptMap));
+    }
+
+    // TODO @puhui999:可以在 CrmCustomerPageReqVO 里面加个 pool 参数,为 true 时,代表来自公海客户的分页
+    @GetMapping("/page")
+    @Operation(summary = "获得客户分页")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<PageResult<CrmCustomerRespVO>> getCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
+        PageResult<CrmCustomerDO> pageResult = customerService.getCustomerPage(pageVO, getLoginUserId());
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty(pageResult.getTotal()));
+        }
+        // 拼接数据
+        // TODO @puhui999:这块的拼接逻辑,可以和 convertPage 合并下;
+//        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+//                convertSetByFlatMap(pageResult.getList(), user -> Stream.of(NumberUtil.parseLong(user.getCreator()), user.getOwnerUserId())));
+//        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+//                convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        return convertPage(customerService.getCustomerPage(pageVO, getLoginUserId()));
+    }
+
+    // TODO @puhui999:
+    @GetMapping("/pool-page")
+    @Operation(summary = "获得公海客户分页")
+    @PreAuthorize("@ss.hasPermission('crm:customer:query')")
+    public CommonResult<PageResult<CrmCustomerRespVO>> getPoolCustomerPage(@Valid CrmCustomerPageReqVO pageVO) {
+        return convertPage(customerService.getCustomerPage(pageVO, CrmPermissionDO.POOL_USER_ID));
+    }
+
+    private CommonResult<PageResult<CrmCustomerRespVO>> convertPage(PageResult<CrmCustomerDO> pageResult) {
+        // 2. 拼接数据
+        Set<Long> ids = convertSet(pageResult.getList(), CrmCustomerDO::getId);
+        // 2.1 获取负责人
+        List<CrmPermissionDO> ownerList = permissionService.getPermissionByBizTypeAndBizIdsAndLevel(
+                CrmBizTypeEnum.CRM_CUSTOMER.getType(), ids, CrmPermissionLevelEnum.OWNER.getLevel());
+        Map<Long, CrmPermissionDO> ownerMap = convertMap(ownerList, CrmPermissionDO::getBizId);
+        // 2.2 获取负责人详情
+        Set<Long> userIds = convertSet(ownerList, CrmPermissionDO::getUserId);
+        userIds.addAll(convertSet(pageResult.getList(), item -> Long.parseLong(item.getCreator()))); // 加入创建者
+        List<AdminUserRespDTO> userList = adminUserApi.getUserList(userIds);
+        Map<Long, AdminUserRespDTO> userMap = convertMap(userList, AdminUserRespDTO::getId);
+        // 2.3 获取部门详情
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userList, AdminUserRespDTO::getDeptId));
+        return success(CrmCustomerConvert.INSTANCE.convertPage(pageResult, ownerMap, userMap, deptMap));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出客户 Excel")
+    @PreAuthorize("@ss.hasPermission('crm:customer:export')")
+    @OperateLog(type = EXPORT)
+    public void exportCustomerExcel(@Valid CrmCustomerExportReqVO exportReqVO,
+                                    HttpServletResponse response) throws IOException {
+        List<CrmCustomerDO> list = customerService.getCustomerList(exportReqVO);
+        // 导出 Excel
+        List<CrmCustomerExcelVO> datas = CrmCustomerConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "客户.xls", "数据", CrmCustomerExcelVO.class, datas);
+    }
+
+    @PutMapping("/transfer")
+    @Operation(summary = "客户转移")
+    @PreAuthorize("@ss.hasPermission('crm:customer:update')")
+    public CommonResult<Boolean> transfer(@Valid @RequestBody CrmCustomerTransferReqVO reqVO) {
+        customerService.transferCustomer(reqVO, getLoginUserId());
+        return success(true);
+    }
+
+    // TODO @Joey:单独建一个属于自己业务的 ReqVO;因为前端如果模拟请求,是不是可以更新其它字段了;
+    @PutMapping("/lock")
+    @Operation(summary = "锁定/解锁客户")
+    @PreAuthorize("@ss.hasPermission('crm:customer:update')")
+    public CommonResult<Boolean> lockCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
+        customerService.lockCustomer(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/receive")
+    @Operation(summary = "领取公海客户")
+    // TODO @xiaqing:1)receiveCustomer 方法名字;2)cIds 改成 ids,要加下 @RequestParam,还有 swagger 注解;3)参数非空,使用 validator 校验;4)返回 true 即可;
+    @PreAuthorize("@ss.hasPermission('crm:customer:receive')")
+    public CommonResult<String> receiveByIds(List<Long> cIds){
+        // 判断是否为空
+        if(CollectionUtils.isEmpty(cIds))
+            return error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),GlobalErrorCodeConstants.BAD_REQUEST.getMsg());
+        // 领取公海任务
+        // TODO @xiaqing:userid,通过 controller 传递给 service,不要在 service 里面获取,无状态
+        customerService.receive(cIds);
+        return success("领取成功");
+    }
+
+    // TODO @xiaqing:1)distributeCustomer 方法名;2)cIds 同上;3)参数校验,同上;4)ownerId 改成 ownerUserId,和别的模块统一;5)返回 true 即可;
+    @PutMapping("/distributeByIds")
+    @Operation(summary = "分配公海给对应负责人")
+    @PreAuthorize("@ss.hasPermission('crm:customer:distributeByIds')")
+    public CommonResult<String> distributeByIds(Long ownerId,List<Long>cIds){
+        //判断参数不能为空
+        if(ownerId==null || CollectionUtils.isEmpty(cIds))
+            return error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),GlobalErrorCodeConstants.BAD_REQUEST.getMsg());
+        customerService.distributeByIds(cIds,ownerId);
+        return success("分配成功");
+    }
+
+}

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

@@ -0,0 +1,98 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigCreateReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerLimitConfigUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerLimitConfigConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customerlimitconfig.CrmCustomerLimitConfigDO;
+import cn.iocoder.yudao.module.crm.service.customerlimitconfig.CrmCustomerLimitConfigService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
+
+@Tag(name = "管理后台 - 客户限制配置")
+@RestController
+@RequestMapping("/crm/customer-limit-config")
+@Validated
+public class CrmCustomerLimitConfigController {
+
+    @Resource
+    private CrmCustomerLimitConfigService customerLimitConfigService;
+
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建客户限制配置")
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:create')")
+    public CommonResult<Long> createCustomerLimitConfig(@Valid @RequestBody CrmCustomerLimitConfigCreateReqVO createReqVO) {
+        return success(customerLimitConfigService.createCustomerLimitConfig(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新客户限制配置")
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:update')")
+    public CommonResult<Boolean> updateCustomerLimitConfig(@Valid @RequestBody CrmCustomerLimitConfigUpdateReqVO updateReqVO) {
+        customerLimitConfigService.updateCustomerLimitConfig(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除客户限制配置")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:delete')")
+    public CommonResult<Boolean> deleteCustomerLimitConfig(@RequestParam("id") Long id) {
+        customerLimitConfigService.deleteCustomerLimitConfig(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得客户限制配置")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:query')")
+    public CommonResult<CrmCustomerLimitConfigRespVO> getCustomerLimitConfig(@RequestParam("id") Long id) {
+        CrmCustomerLimitConfigDO customerLimitConfig = customerLimitConfigService.getCustomerLimitConfig(id);
+        // 拼接数据
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(customerLimitConfig.getUserIds());
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(customerLimitConfig.getDeptIds());
+        return success(CrmCustomerLimitConfigConvert.INSTANCE.convert(customerLimitConfig, userMap, deptMap));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得客户限制配置分页")
+    @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:query')")
+    public CommonResult<PageResult<CrmCustomerLimitConfigRespVO>> getCustomerLimitConfigPage(@Valid CrmCustomerLimitConfigPageReqVO pageVO) {
+        PageResult<CrmCustomerLimitConfigDO> pageResult = customerLimitConfigService.getCustomerLimitConfigPage(pageVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty(pageResult.getTotal()));
+        }
+        // 拼接数据
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getUserIds, Collection::stream));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
+                convertSetByFlatMap(pageResult.getList(), CrmCustomerLimitConfigDO::getDeptIds, Collection::stream));
+        return success(CrmCustomerLimitConfigConvert.INSTANCE.convertPage(pageResult, userMap, deptMap));
+    }
+
+}

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

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPoolConfigRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPoolConfigUpdateReqVO;
+import cn.iocoder.yudao.module.crm.convert.customer.CrmCustomerConvert;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerPoolConfigDO;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerPoolConfigService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - CRM 客户公海配置")
+@RestController
+@RequestMapping("/crm/customer-pool-config")
+@Validated
+public class CrmCustomerPoolConfigController {
+
+    @Resource
+    private CrmCustomerPoolConfigService customerPoolConfigService;
+
+    @GetMapping("/get")
+    @Operation(summary = "获取客户公海规则设置")
+    @PreAuthorize("@ss.hasPermission('crm:customer-pool-config:query')")
+    public CommonResult<CrmCustomerPoolConfigRespVO> getCustomerPoolConfig() {
+        CrmCustomerPoolConfigDO customerPoolConfig = customerPoolConfigService.getCustomerPoolConfig();
+        return success(CrmCustomerConvert.INSTANCE.convert(customerPoolConfig));
+    }
+
+    // TODO @wanwan:这个请求,搞成 save 哈;
+    @PutMapping("/update")
+    @Operation(summary = "更新客户公海规则设置")
+    @PreAuthorize("@ss.hasPermission('crm:customer-pool-config:update')")
+    public CommonResult<Boolean> updateCustomerPoolConfig(@Valid @RequestBody CrmCustomerPoolConfigUpdateReqVO updateReqVO) {
+        customerPoolConfigService.updateCustomerPoolConfig(updateReqVO);
+        return success(true);
+    }
+
+}

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

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.framework.common.validation.Mobile;
+import cn.iocoder.yudao.framework.common.validation.Telephone;
+import cn.iocoder.yudao.module.crm.enums.customer.CrmCustomerLevelEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 客户 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmCustomerBaseVO {
+
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
+    @NotEmpty(message = "客户名称不能为空")
+    private String name;
+
+    @Schema(description = "所属行业", example = "1")
+    private Integer industryId;
+
+    @Schema(description = "客户等级", example = "2")
+    @InEnum(CrmCustomerLevelEnum.class)
+    private Integer level;
+
+    @Schema(description = "客户来源", example = "3")
+    private Integer source;
+
+    @Schema(description = "手机", example = "18000000000")
+    @Mobile
+    private String mobile;
+
+    @Schema(description = "电话", example = "18000000000")
+    @Telephone
+    private String telephone;
+
+    @Schema(description = "网址", example = "https://www.baidu.com")
+    private String website;
+
+    @Schema(description = "QQ", example = "123456789")
+    @Size(max = 20, message = "QQ长度不能超过 20 个字符")
+    private String qq;
+
+    @Schema(description = "wechat", example = "123456789")
+    @Size(max = 255, message = "微信长度不能超过 255 个字符")
+    private String wechat;
+
+    @Schema(description = "email", example = "123456789@qq.com")
+    @Email(message = "邮箱格式不正确")
+    @Size(max = 255, message = "邮箱长度不能超过 255 个字符")
+    private String email;
+
+    @Schema(description = "客户描述", example = "任意文字")
+    @Size(max = 4096, message = "客户描述长度不能超过 4096 个字符")
+    private String description;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "地区编号", example = "20158")
+    private Integer areaId;
+
+    @Schema(description = "详细地址", example = "北京市海淀区")
+    private String detailAddress;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime contactNextTime;
+
+}

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

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - CRM 客户创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerCreateReqVO extends CrmCustomerBaseVO {
+
+    @Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    @NotNull(message = "负责人不能为空")
+    private Long ownerUserId;
+
+}

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

@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+// TODO 芋艿:导出最后做,等基本确认的差不多之后;
+/**
+ * CRM 客户 Excel VO
+ *
+ * @author Wanwan
+ */
+@Data
+public class CrmCustomerExcelVO {
+
+    @ExcelProperty("编号")
+    private Long id;
+
+    @ExcelProperty("客户名称")
+    private String name;
+
+    @ExcelProperty(value = "跟进状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean followUpStatus;
+
+    @ExcelProperty(value = "锁定状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean lockStatus;
+
+    @ExcelProperty(value = "成交状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.BOOLEAN_STRING)
+    private Boolean dealStatus;
+
+    @ExcelProperty(value = "所属行业", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY)
+    private Integer industryId;
+
+    @ExcelProperty(value = "客户等级", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL)
+    private Integer level;
+
+    @ExcelProperty(value = "客户来源", converter = DictConvert.class)
+    @DictFormat(cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE)
+    private Integer source;
+
+    @ExcelProperty("手机")
+    private String mobile;
+
+    @ExcelProperty("电话")
+    private String telephone;
+
+    @ExcelProperty("网址")
+    private String website;
+
+    @ExcelProperty("QQ")
+    private String qq;
+
+    @ExcelProperty("wechat")
+    private String wechat;
+
+    @ExcelProperty("email")
+    private String email;
+
+    @ExcelProperty("客户描述")
+    private String description;
+
+    @ExcelProperty("备注")
+    private String remark;
+
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+
+    @ExcelProperty("地区编号")
+    private Integer areaId;
+
+    @ExcelProperty("详细地址")
+    private String detailAddress;
+
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

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

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+// TODO 芋艿:导出最后做,等基本确认的差不多之后;
+@Schema(description = "管理后台 - CRM 客户 Excel 导出 Request VO,参数和 CrmCustomerPageReqVO 是一致的")
+@Data
+public class CrmCustomerExportReqVO {
+
+    @Schema(description = "客户名称", example = "赵六")
+    private String name;
+
+    @Schema(description = "手机", example = "18000000000")
+    private String mobile;
+
+}

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

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+// TODO @wanwan:vo 下,可以新建一个 limitconfig,放它的 vo;
+/**
+ * 客户限制配置 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class CrmCustomerLimitConfigBaseVO {
+
+    @Schema(description = "规则类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "规则类型不能为空")
+    private Integer type;
+
+    @Schema(description = "规则适用人群")
+    private List<Long> userIds;
+
+    @Schema(description = "规则适用部门")
+    private List<Long> deptIds;
+
+    @Schema(description = "数量上限", requiredMode = Schema.RequiredMode.REQUIRED, example = "28384")
+    @NotNull(message = "数量上限不能为空")
+    private Integer maxCount;
+
+    @Schema(description = "成交客户是否占有拥有客户数(当 type = 1 时)")
+    private Boolean dealCountEnabled;
+
+}

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

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 客户限制配置创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CrmCustomerLimitConfigCreateReqVO extends CrmCustomerLimitConfigBaseVO {
+
+}

部分文件因为文件数量过多而无法显示