Browse Source

Merge remote-tracking branch 'origin/develop' into develop

# Conflicts:
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/article/AppArticleController.java
owen 1 year ago
parent
commit
9203f485e9
100 changed files with 1052 additions and 748 deletions
  1. 11 5
      README.md
  2. 2 3
      sql/mysql/optinal/crm.sql
  3. 4 4
      yudao-dependencies/pom.xml
  4. 1 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java
  5. 5 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java
  6. 0 2
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java
  7. 32 1
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv
  8. 1 0
      yudao-framework/yudao-spring-boot-starter-biz-operatelog/pom.xml
  9. 1 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  10. 1 0
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java
  11. 12 1
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java
  12. 4 1
      yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
  13. 1 1
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java
  14. 3 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java
  15. 6 0
      yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml
  16. 14 14
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  17. 11 1
      yudao-framework/yudao-spring-boot-starter-security/pom.xml
  18. 27 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogV2Configuration.java
  19. 4 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/package-info.java
  20. 85 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/ILogRecordServiceImpl.java
  21. 7 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/package-info.java
  22. 2 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  23. 19 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java
  24. 0 11
      yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
  25. 1 1
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java
  26. 4 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  27. 4 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
  28. 0 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  29. 0 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/package-info.java
  30. 4 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
  31. 8 2
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java
  32. 3 3
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLimitConfigTypeEnum.java
  33. 27 0
      yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionRoleCodeEnum.java
  34. 33 49
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
  35. 3 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessPageReqVO.java
  36. 35 11
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/CrmContactController.java
  37. 15 15
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactBaseVO.java
  38. 22 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactBusinessReqVO.java
  39. 0 115
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contactbusinesslink/CrmContactBusinessLinkController.java
  40. 0 30
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contactbusinesslink/vo/CrmContactBusinessLinkPageReqVO.java
  41. 0 26
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contactbusinesslink/vo/CrmContactBusinessLinkRespVO.java
  42. 0 23
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contactbusinesslink/vo/CrmContactBusinessLinkSaveReqVO.java
  43. 7 7
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
  44. 2 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.http
  45. 22 21
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
  46. 1 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerLimitConfigController.java
  47. 1 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerPoolConfigController.java
  48. 16 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerLockReqVO.java
  49. 20 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerOperateLogPageReqVO.java
  50. 2 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/CrmCustomerTransferReqVO.java
  51. 3 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/poolconfig/CrmCustomerPoolConfigSaveReqVO.java
  52. 4 3
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.java
  53. 8 8
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
  54. 4 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
  55. 5 7
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java
  56. 21 48
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/CrmContactConvert.java
  57. 0 16
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contactbusinessslink/CrmContactBusinessLinkConvert.java
  58. 8 15
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/CrmContractConvert.java
  59. 0 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java
  60. 9 18
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivableConvert.java
  61. 10 20
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivablePlanConvert.java
  62. 0 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/package-info.java
  63. 5 7
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/CrmContactBusinessDO.java
  64. 19 14
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/CrmContactDO.java
  65. 6 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java
  66. 81 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/followup/CrmFollowUpRecordDO.java
  67. 9 5
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
  68. 1 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java
  69. 1 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contact/CrmContactMapper.java
  70. 0 30
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contactbusinesslink/CrmContactBusinessLinkMapper.java
  71. 34 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contactbusinesslink/CrmContactBusinessMapper.java
  72. 1 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractMapper.java
  73. 13 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerLimitConfigMapper.java
  74. 15 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
  75. 1 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
  76. 1 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java
  77. 0 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/package-info.java
  78. 0 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/package-info.java
  79. 5 14
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmIndustryParseFunction.java
  80. 5 13
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmLevelParseFunction.java
  81. 5 13
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmSourceParseFunction.java
  82. 1 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/package-info.java
  83. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/annotations/CrmPermission.java
  84. 25 17
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/aop/CrmPermissionAspect.java
  85. 1 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/package-info.java
  86. 40 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/util/CrmPermissionUtils.java
  87. 1 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/package-info.java
  88. 12 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
  89. 40 16
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
  90. 10 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessStatusService.java
  91. 6 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessStatusServiceImpl.java
  92. 10 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessStatusTypeService.java
  93. 7 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessStatusTypeServiceImpl.java
  94. 0 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/package-info.java
  95. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
  96. 0 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/package-info.java
  97. 38 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactBusinessService.java
  98. 83 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactBusinessServiceImpl.java
  99. 19 7
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java
  100. 0 72
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contactbusinesslink/CrmContactBusinessLinkService.java

+ 11 - 5
README.md

@@ -31,7 +31,7 @@
 
 ![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
 
-* Java 后端:`master` 分支为 JDK 21 + Spring Boot 3.2.0,`master-jdk8` 分支为 JDK8 + Spring Boot 2.7.18
+* Java 后端:`master` 分支为 JDK 8 + Spring Boot 2.7.18,`master-jdk21` 分支为 JDK21 + Spring Boot 3.2.0
 * 管理后台的电脑端:Vue3 提供 `element-plus`、`vben(ant-design-vue)` 两个版本,Vue2 提供 `element-ui` 版本
 * 管理后台的移动端:采用 `uni-app` 方案,一份代码多终端适配,同时支持 APP、小程序、H5!
 * 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson
@@ -78,15 +78,21 @@
 
 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM 等功能
 
-* JDK 21 + Spring Boot 3.2.0 版本:<https://gitee.com/zhijiantianya/ruoyi-vue-pro> 的 `master` 分支
-* JDK 8 + Spring Boot 2.7.18 版本:<https://gitee.com/zhijiantianya/ruoyi-vue-pro> 的 `master-jdk8` 分支
+* JDK 8 + Spring Boot 2.7.18 版本:<https://gitee.com/zhijiantianya/ruoyi-vue-pro> 的 `master` 分支
+* JDK 21 + Spring Boot 3.2.0 版本:<https://gitee.com/zhijiantianya/ruoyi-vue-pro> 的 `master-jdk21` 分支
+
+两个分支的功能是一致的,可以放心使用!
 
 ### ➡️️ 精简版
 
 【精简版】只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM 等功能
 
-* JDK 21 + Spring Boot 3.2.0 版本:<https://gitee.com/yudaocode/yudao-boot-mini> 的 `master` 分支
-* JDK 8 + Spring Boot 2.7.18 版本:<https://gitee.com/yudaocode/yudao-boot-mini> 的 `master-jdk8` 分支
+* JDK 8 + Spring Boot 2.7.18 版本:<https://gitee.com/yudaocode/yudao-boot-mini> 的 `master` 分支
+* JDK 21 + Spring Boot 3.2.0 版本:<https://gitee.com/yudaocode/yudao-boot-mini> 的 `master-jdk21` 分支
+
+如果你想把【完整版】的功能,迁移到【精简版】,可以参考 [《迁移功能到精简版》](https://doc.iocoder.cn/migrate-module/) 文档。
+
+如果你想把【完整版】的功能,迁移到【精简版】,可以参考 [《迁移功能到精简版》](https://doc.iocoder.cn/migrate-module/) 文档。
 
 ## 😎 开源协议
 

+ 2 - 3
sql/mysql/optinal/crm.sql

@@ -1,6 +1,6 @@
 -- `ruoyi-vue-pro`.crm_contact_business_link definition
 
-CREATE TABLE `crm_contact_business_link` (
+CREATE TABLE `crm_contact_business` (
                                              `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
                                              `contact_id` int(11) DEFAULT NULL COMMENT '联系人id',
                                              `business_id` int(11) DEFAULT NULL COMMENT '商机id',
@@ -10,6 +10,5 @@ CREATE TABLE `crm_contact_business_link` (
                                              `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
                                              `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
                                              `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '租户编号',
-                                             PRIMARY KEY (`id`),
-                                             UNIQUE KEY `crm_contact_business_link_un` (`contact_id`,`business_id`,`deleted`,`tenant_id`)
+                                             PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='联系人商机关联表';

+ 4 - 4
yudao-dependencies/pom.xml

@@ -26,7 +26,7 @@
         <mybatis-plus.version>3.5.4.1</mybatis-plus.version>
         <mybatis-plus-generator.version>3.5.4.1</mybatis-plus-generator.version>
         <dynamic-datasource.version>4.2.0</dynamic-datasource.version>
-        <mybatis-plus-join.version>1.4.7.2</mybatis-plus-join.version>
+        <mybatis-plus-join.version>1.4.8.1</mybatis-plus-join.version>
         <redisson.version>3.25.0</redisson.version>
         <dm8.jdbc.version>8.1.3.62</dm8.jdbc.version>
         <!-- 消息队列 -->
@@ -51,7 +51,7 @@
         <mapstruct.version>1.5.5.Final</mapstruct.version>
         <hutool-5.version>5.8.23</hutool-5.version>
         <hutool-6.version>6.0.0-M8</hutool-6.version>
-        <easyexcel.verion>3.3.2</easyexcel.verion>
+        <easyexcel.verion>3.3.3</easyexcel.verion>
         <velocity.version>2.3</velocity.version>
         <screw.version>1.0.5</screw.version>
         <fastjson.version>1.2.83</fastjson.version>
@@ -72,7 +72,7 @@
         <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
         <tencentcloud-sdk-java.version>3.1.880</tencentcloud-sdk-java.version>
         <justauth.version>2.0.5</justauth.version>
-        <jimureport.version>1.6.1</jimureport.version>
+        <jimureport.version>1.6.6-beta2</jimureport.version>
         <xercesImpl.version>2.12.2</xercesImpl.version>
         <weixin-java.version>4.5.7.B</weixin-java.version>
         <ureport2.version>2.2.9</ureport2.version>
@@ -651,7 +651,7 @@
             <!-- 积木报表-->
             <dependency>
                 <groupId>org.jeecgframework.jimureport</groupId>
-                <artifactId>jimureport-spring-boot-starter</artifactId>
+                <artifactId>jimureport-spring-boot3-starter</artifactId>
                 <version>${jimureport.version}</version>
                 <exclusions>
                     <exclusion>

+ 1 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java

@@ -15,6 +15,7 @@ import java.util.Arrays;
 @Getter
 public enum TerminalEnum implements IntArrayValuable {
 
+    UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
     WECHAT_MINI_PROGRAM(10, "微信小程序"),
     WECHAT_WAP(11, "微信公众号"),
     H5(20, "H5 网页"),

+ 5 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java

@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.framework.common.util.cache;
 
+import com.alibaba.ttl.threadpool.TtlExecutors;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 
 import java.time.Duration;
+import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 /**
@@ -15,11 +17,13 @@ import java.util.concurrent.Executors;
 public class CacheUtils {
 
     public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
+        Executor executor = Executors.newCachedThreadPool(  // TODO 芋艿:可能要思考下,未来要不要做成可配置
+                TtlExecutors.getDefaultDisableInheritableThreadFactory()); // TTL 保证 ThreadLocal 可以透传
         return CacheBuilder.newBuilder()
                 // 只阻塞当前数据加载线程,其他线程返回旧值
                 .refreshAfterWrite(duration)
                 // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
-                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置
+                .build(CacheLoader.asyncReloading(loader, executor));
     }
 
 }

+ 0 - 2
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java

@@ -88,8 +88,6 @@ public class ServletUtils {
         return JakartaServletUtil.getClientIP(request);
     }
 
-    // TODO @疯狂:terminal 还是从 ServletUtils 里拿,更容易全局治理;
-
     public static boolean isJsonRequest(ServletRequest request) {
         return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
     }

+ 32 - 1
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv

@@ -490,6 +490,37 @@ id,name,type,parentId
 441700,阳江市,3,440000
 441800,清远市,3,440000
 441900,东莞市,3,440000
+441901,莞城区,4,441900
+441902,南城区,4,441900
+441904,万江区,4,441900
+441905,石碣镇,4,441900
+441906,石龙镇,4,441900
+441907,茶山镇,4,441900
+441908,石排镇,4,441900
+441909,企石镇,4,441900
+441910,横沥镇,4,441900
+441911,桥头镇,4,441900
+441912,谢岗镇,4,441900
+441913,东坑镇,4,441900
+441914,常平镇,4,441900
+441915,寮步镇,4,441900
+441916,大朗镇,4,441900
+441917,麻涌镇,4,441900
+441918,中堂镇,4,441900
+441919,高埗镇,4,441900
+441920,樟木头镇,4,441900
+441921,大岭山镇,4,441900
+441922,望牛墩镇,4,441900
+441923,黄江镇,4,441900
+441924,洪梅镇,4,441900
+441925,清溪镇,4,441900
+441926,沙田镇,4,441900
+441927,道滘镇,4,441900
+441928,塘厦镇,4,441900
+441929,虎门镇,4,441900
+441930,厚街镇,4,441900
+441931,凤岗镇,4,441900
+441932,长安镇,4,441900
 442000,中山市,3,440000
 445100,潮州市,3,440000
 445200,揭阳市,3,440000
@@ -3605,4 +3636,4 @@ id,name,type,parentId
 659008,可克达拉市,4,659000
 659009,昆玉市,4,659000
 659010,胡杨河市,4,659000
-659011,新星市,4,659000
+659011,新星市,4,659000

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-biz-operatelog/pom.xml

@@ -46,6 +46,7 @@
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
         </dependency>
+
     </dependencies>
 
 </project>

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

@@ -71,6 +71,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
             payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
         }
+//        payConfig.setCertSerialNo();
 
         // 创建 client 客户端
         client = new WxPayServiceImpl();

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java

@@ -41,6 +41,7 @@ public abstract class AbstractSmsClient implements SmsClient {
             return;
         }
         log.info("[refresh][配置({})发生变化,重新初始化]", properties);
+        this.properties = properties;
         // 初始化
         this.init();
     }

+ 12 - 1
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.tenant.core.context;
 
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
 import com.alibaba.ttl.TransmittableThreadLocal;
 
@@ -21,7 +22,7 @@ public class TenantContextHolder {
     private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
 
     /**
-     * 获得租户编号
+     * 获得租户编号
      *
      * @return 租户编号
      */
@@ -29,6 +30,16 @@ public class TenantContextHolder {
         return TENANT_ID.get();
     }
 
+    /**
+     * 获得租户编号 String
+     *
+     * @return 租户编号
+     */
+    public static String getTenantIdStr() {
+        Long tenantId = getTenantId();
+        return StrUtil.toStringOrNull(tenantId);
+    }
+
     /**
      * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
      *

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

@@ -1,12 +1,14 @@
 package cn.iocoder.yudao.framework.excel.core.util;
 
 import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.converters.longconverter.LongStringConverter;
 import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
 import org.springframework.web.multipart.MultipartFile;
 
 import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 /**
@@ -33,9 +35,10 @@ public class ExcelUtils {
         EasyExcel.write(response.getOutputStream(), head)
                 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
                 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
+                .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
                 .sheet(sheetName).doWrite(data);
         // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
-        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
+        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
         response.setContentType("application/vnd.ms-excel;charset=UTF-8");
     }
 

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java

@@ -81,7 +81,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
         }
         // 腾讯云必须有 region,否则会报错
         if (config.getEndpoint().contains(ENDPOINT_TENCENT)) {
-            return StrUtil.subAfter(config.getEndpoint(), ".cos.", false)
+            return StrUtil.subAfter(config.getEndpoint(), "cos.", false)
                     .replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint
         }
         return null;

+ 3 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.flowable.config;
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
 import cn.iocoder.yudao.framework.flowable.core.web.FlowableWebFilter;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.core.task.AsyncListenableTaskExecutor;
@@ -17,6 +18,7 @@ public class YudaoFlowableConfiguration {
      * 如果不创建,会导致项目启动时,Flowable 报错的问题
      */
     @Bean
+    @ConditionalOnMissingBean
     public AsyncListenableTaskExecutor taskExecutor() {
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
         executor.setCorePoolSize(8);
@@ -40,4 +42,5 @@ public class YudaoFlowableConfiguration {
         registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER);
         return registrationBean;
     }
+
 }

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml

@@ -65,6 +65,12 @@
         <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-undertow</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>

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

@@ -146,8 +146,8 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
      *
      * @param entities 实体们
      */
-    default void insertBatch(Collection<T> entities) {
-        Db.saveBatch(entities);
+    default Boolean insertBatch(Collection<T> entities) {
+        return Db.saveBatch(entities);
     }
 
     /**
@@ -156,28 +156,28 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
      * @param entities 实体们
      * @param size     插入数量 Db.saveBatch 默认为 1000
      */
-    default void insertBatch(Collection<T> entities, int size) {
-        Db.saveBatch(entities, size);
+    default Boolean insertBatch(Collection<T> entities, int size) {
+        return Db.saveBatch(entities, size);
     }
 
-    default void updateBatch(T update) {
-        update(update, new QueryWrapper<>());
+    default int updateBatch(T update) {
+        return update(update, new QueryWrapper<>());
     }
 
-    default void updateBatch(Collection<T> entities) {
-        Db.updateBatchById(entities);
+    default Boolean updateBatch(Collection<T> entities) {
+        return Db.updateBatchById(entities);
     }
 
-    default void updateBatch(Collection<T> entities, int size) {
-        Db.updateBatchById(entities, size);
+    default Boolean updateBatch(Collection<T> entities, int size) {
+        return Db.updateBatchById(entities, size);
     }
 
-    default void insertOrUpdate(T entity) {
-        Db.saveOrUpdate(entity);
+    default Boolean insertOrUpdate(T entity) {
+        return  Db.saveOrUpdate(entity);
     }
 
-    default void insertOrUpdateBatch(Collection<T> collection) {
-        Db.saveOrUpdateBatch(collection);
+    default Boolean insertOrUpdateBatch(Collection<T> collection) {
+        return Db.saveOrUpdateBatch(collection);
     }
 
     default int delete(String field, String value) {

+ 11 - 1
yudao-framework/yudao-spring-boot-starter-security/pom.xml

@@ -12,7 +12,10 @@
     <packaging>jar</packaging>
 
     <name>${project.artifactId}</name>
-    <description>用户的认证、权限的校验</description>
+    <description>
+        1. security:用户的认证、权限的校验,实现「谁」可以做「什么事」
+        2. operatelog:操作日志,实现「谁」在「什么时间」对「什么」做了「什么事」
+    </description>
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <dependencies>
@@ -50,6 +53,13 @@
             <artifactId>guava</artifactId>
         </dependency>
 
+        <dependency>
+            <!-- Spring Boot 通用操作日志组件,基于注解实现 -->
+            <!-- 此组件解决的问题是:「谁」在「什么时间」对「什么」做了「什么事」 -->
+            <groupId>io.github.mouzt</groupId>
+            <artifactId>bizlog-sdk</artifactId>
+        </dependency>
+
         <!-- 业务组件 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>

+ 27 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogV2Configuration.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.framework.operatelog.config;
+
+import cn.iocoder.yudao.framework.operatelog.core.service.ILogRecordServiceImpl;
+import com.mzt.logapi.service.ILogRecordService;
+import com.mzt.logapi.starter.annotation.EnableLogRecord;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+
+/**
+ * 操作日志配置类
+ *
+ * @author HUIHUI
+ */
+@EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦
+@AutoConfiguration
+@Slf4j
+public class YudaoOperateLogV2Configuration {
+
+    @Bean
+    @Primary
+    public ILogRecordService iLogRecordServiceImpl() {
+        return new ILogRecordServiceImpl();
+    }
+
+}

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * 占位,无特殊作用
+ */
+package cn.iocoder.yudao.framework.operatelog.core;

+ 85 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/ILogRecordServiceImpl.java

@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.framework.operatelog.core.service;
+
+import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
+import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
+import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2CreateReqDTO;
+import com.mzt.logapi.beans.LogRecord;
+import com.mzt.logapi.service.ILogRecordService;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+// TODO @puhui999:LogRecordServiceImpl 改成这个名字哈
+/**
+ * 操作日志 ILogRecordService 实现类
+ *
+ * 基于 {@link OperateLogApi} 实现,记录操作日志
+ *
+ * @author HUIHUI
+ */
+@Slf4j
+public class ILogRecordServiceImpl implements ILogRecordService {
+
+    @Resource
+    private OperateLogApi operateLogApi;
+
+    @Override
+    public void record(LogRecord logRecord) {
+        // 1. 补全通用字段
+        OperateLogV2CreateReqDTO reqDTO = new OperateLogV2CreateReqDTO();
+        reqDTO.setTraceId(TracerUtils.getTraceId());
+        // 补充用户信息
+        fillUserFields(reqDTO);
+        // 补全模块信息
+        fillModuleFields(reqDTO, logRecord);
+        // 补全请求信息
+        fillRequestFields(reqDTO);
+
+        // 2. 异步记录日志
+        operateLogApi.createOperateLogV2(reqDTO);
+        // TODO 测试结束删除或搞个开关
+        log.info("操作日志 ===> {}", reqDTO);
+    }
+
+    private static void fillUserFields(OperateLogV2CreateReqDTO reqDTO) {
+        // TODO @puhui999:使用 SecurityFrameworkUtils。因为要考虑,rpc、mq、job,它其实不是 web;
+        reqDTO.setUserId(WebFrameworkUtils.getLoginUserId());
+        reqDTO.setUserType(WebFrameworkUtils.getLoginUserType());
+    }
+
+    public static void fillModuleFields(OperateLogV2CreateReqDTO reqDTO, LogRecord logRecord) {
+        reqDTO.setType(logRecord.getType()); // 大模块类型,例如:CRM 客户
+        reqDTO.setSubType(logRecord.getSubType());// 操作名称,例如:转移客户
+        reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 业务编号,例如:客户编号
+        reqDTO.setAction(logRecord.getAction());// 操作内容,例如:修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。
+        reqDTO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"}
+    }
+
+    private static void fillRequestFields(OperateLogV2CreateReqDTO reqDTO) {
+        // 获得 Request 对象
+        HttpServletRequest request = ServletUtils.getRequest();
+        if (request == null) {
+            return;
+        }
+        // 补全请求信息
+        reqDTO.setRequestMethod(request.getMethod());
+        reqDTO.setRequestUrl(request.getRequestURI());
+        reqDTO.setUserIp(ServletUtils.getClientIP(request));
+        reqDTO.setUserAgent(ServletUtils.getUserAgent(request));
+    }
+
+    @Override
+    public List<LogRecord> queryLog(String bizNo, String type) {
+        throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
+    }
+
+    @Override
+    public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) {
+        throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询");
+    }
+
+}

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * 基于 mzt-log 框架
+ * 实现操作日志功能
+ *
+ * @author HUIHUI
+ */
+package cn.iocoder.yudao.framework.operatelog;

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

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

+ 19 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java

@@ -1,8 +1,11 @@
 package cn.iocoder.yudao.framework.web.core.util;
 
 import cn.hutool.core.util.NumberUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import org.springframework.web.context.request.RequestAttributes;
 import org.springframework.web.context.request.RequestContextHolder;
@@ -25,6 +28,13 @@ public class WebFrameworkUtils {
 
     public static final String HEADER_TENANT_ID = "tenant-id";
 
+    /**
+     * 终端的 Header
+     *
+     * @see cn.iocoder.yudao.framework.common.enums.TerminalEnum
+     */
+    public static final String HEADER_TERMINAL = "terminal";
+
     private static WebProperties properties;
 
     public WebFrameworkUtils(WebProperties webProperties) {
@@ -107,6 +117,15 @@ public class WebFrameworkUtils {
         return getLoginUserId(request);
     }
 
+    public static Integer getTerminal() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return TerminalEnum.UNKNOWN.getTerminal();
+        }
+        String terminalValue = request.getHeader(HEADER_TERMINAL);
+        return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal());
+    }
+
     public static void setCommonResult(ServletRequest request, CommonResult<?> result) {
         request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result);
     }

+ 0 - 11
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml

@@ -38,17 +38,6 @@
             <artifactId>spring-boot-starter-websocket</artifactId>
         </dependency>
 
-        <!-- Web 相关 -->
-        <dependency>
-            <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
-                 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
-                 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
-            -->
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-security</artifactId>
-            <scope>provided</scope>
-        </dependency>
-
         <!-- 消息队列相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java

@@ -66,7 +66,7 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
     @Override
     public void removeSession(WebSocketSession session) {
         // 移除从 idSessions 中
-        idSessions.remove(session.getId(), session);
+        idSessions.remove(session.getId());
         // 移除从 idSessions 中
         LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
         if (user == null) {

+ 4 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java

@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
 import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
@@ -71,7 +72,8 @@ public class BpmModelServiceImpl implements BpmModelService {
             modelQuery.modelCategory(pageVO.getCategory());
         }
         // 执行查询
-        List<Model> models = modelQuery.orderByCreateTime().desc()
+        List<Model> models = modelQuery.modelTenantId(TenantContextHolder.getTenantIdStr())
+                .orderByCreateTime().desc()
                 .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
 
         // 获得 Form Map
@@ -107,6 +109,7 @@ public class BpmModelServiceImpl implements BpmModelService {
         // 创建流程定义
         Model model = repositoryService.newModel();
         BpmModelConvert.INSTANCE.copy(model, createReqVO);
+        model.setTenantId(TenantContextHolder.getTenantIdStr());
         // 保存流程定义
         repositoryService.saveModel(model);
         // 保存 BPMN XML

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

@@ -6,6 +6,8 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
 import cn.iocoder.yudao.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionListReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageItemRespVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
@@ -124,6 +126,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
         Deployment deploy = repositoryService.createDeployment()
                 .key(createReqDTO.getKey()).name(createReqDTO.getName()).category(createReqDTO.getCategory())
                 .addBytes(createReqDTO.getKey() + BPMN_FILE_SUFFIX, createReqDTO.getBpmnBytes())
+                .tenantId(TenantContextHolder.getTenantIdStr())
                 .deploy();
 
         // 设置 ProcessDefinition 的 category 分类
@@ -234,6 +237,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
             definitionQuery.active();
         }
         // 执行查询
+        definitionQuery.processDefinitionTenantId(TenantContextHolder.getTenantIdStr());
         List<ProcessDefinition> processDefinitions = definitionQuery.list();
         if (CollUtil.isEmpty(processDefinitions)) {
             return Collections.emptyList();

File diff suppressed because it is too large
+ 0 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java


+ 0 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/package-info.java

@@ -1 +0,0 @@
-package cn.iocoder.yudao.module.bpm.service.task;

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

@@ -39,6 +39,10 @@ public interface ErrorCodeConstants {
     ErrorCode CUSTOMER_IN_POOL = new ErrorCode(1_020_006_004, "客户【{}】放入公海失败,原因:已经是公海客户");
     ErrorCode CUSTOMER_LOCKED_PUT_POOL_FAIL = new ErrorCode(1_020_006_005, "客户【{}】放入公海失败,原因:客户已锁定");
     ErrorCode CUSTOMER_UPDATE_OWNER_USER_FAIL = new ErrorCode(1_020_006_006, "更新客户【{}】负责人失败, 原因:系统异常");
+    ErrorCode CUSTOMER_LOCK_FAIL_IS_LOCK = new ErrorCode(1_020_006_007, "锁定客户失败,它已经处于锁定状态");
+    ErrorCode CUSTOMER_UNLOCK_FAIL_IS_UNLOCK = new ErrorCode(1_020_006_008, "锁定客户失败,它已经处于未锁定状态");
+    ErrorCode CUSTOMER_LOCK_EXCEED_LIMIT = new ErrorCode(1_020_006_009, "锁定客户失败,超出锁定规则上限");
+    ErrorCode CUSTOMER_OWNER_EXCEED_LIMIT = new ErrorCode(1_020_006_010, "操作失败,超出客户数拥有上限");
 
     // ========== 权限管理 1_020_007_000 ==========
     ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");

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

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.crm.enums;
 
-// TODO 芋艿:操作日志;看看这个类怎么搞个好点的规范;
 /**
  * CRM 操作日志枚举
  *
@@ -22,6 +21,13 @@ public interface LogRecordConstants {
     //======================= 客户转移操作日志 =======================
 
     String TRANSFER_CUSTOMER_LOG_SUCCESS = "把客户【{{#crmCustomer.name}}】的负责人从【{getAdminUserById{#crmCustomer.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
-    String TRANSFER_CUSTOMER_LOG_FAIL = ""; // TODO @puhui999:这个可以删除哈,一般不搞失败的日志
+
+    // TODO @puhui999:这里格式是不是可以这样;目的是:统一管理,也减少 Service 里各种“复杂”字符串
+    // ======================= Customer 客户 =======================
+    String CUSTOMER_TYPE = "CRM 客户";
+    String CUSTOMER_CREATE_SUB_TYPE = "创建客户";
+    String CUSTOMER_CREATE_SUCCESS = "更新了客户{_DIFF{#updateReqVO}}";
+
+    String CUSTOMER_UPDATE_SUB_TYPE = "更新客户";
 
 }

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

@@ -18,19 +18,19 @@ public enum CrmCustomerLimitConfigTypeEnum implements IntArrayValuable {
     /**
      * 拥有客户数限制
      */
-    CUSTOMER_QUANTITY_LIMIT(1, "拥有客户数限制"),
+    CUSTOMER_OWNER_LIMIT(1, "拥有客户数限制"),
     /**
      * 锁定客户数限制
      */
     CUSTOMER_LOCK_LIMIT(2, "锁定客户数限制"),
     ;
 
-    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLimitConfigTypeEnum::getCode).toArray();
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLimitConfigTypeEnum::getType).toArray();
 
     /**
      * 状态
      */
-    private final Integer code;
+    private final Integer type;
     /**
      * 状态名
      */

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

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

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

@@ -7,8 +7,6 @@ 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.business.*;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.status.CrmBusinessStatusQueryVO;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.type.CrmBusinessStatusTypeQueryVO;
 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.business.CrmBusinessStatusDO;
@@ -30,12 +28,10 @@ import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
 import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
+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;
 
@@ -47,16 +43,14 @@ public class CrmBusinessController {
 
     @Resource
     private CrmBusinessService businessService;
-
     @Resource
     private CrmCustomerService customerService;
-
     @Resource
     private CrmBusinessStatusTypeService businessStatusTypeService;
-
     @Resource
     private CrmBusinessStatusService businessStatusService;
 
+    // TODO @商机待定:CrmBusinessCreateReqVO、CrmBusinessUpdateReqVO、CrmBusinessRespVO 按照新的 VO 规范
     @PostMapping("/create")
     @Operation(summary = "创建商机")
     @PreAuthorize("@ss.hasPermission('crm:business:create')")
@@ -95,27 +89,7 @@ public class CrmBusinessController {
     @PreAuthorize("@ss.hasPermission('crm:business:query')")
     public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPage(@Valid CrmBusinessPageReqVO pageVO) {
         PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(pageVO, getLoginUserId());
-        if (CollUtil.isEmpty(pageResult.getList())) {
-            return success(PageResult.empty(pageResult.getTotal()));
-        }
-        // 处理客户名称回显
-        // TODO @ljlleo:可以使用 CollectionUtils.convertSet 替代常用的 stream 操作,更简洁一点;下面几个也是哈;
-        Set<Long> customerIds = pageResult.getList().stream()
-                .map(CrmBusinessDO::getCustomerId).filter(Objects::nonNull).collect(Collectors.toSet());
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(customerIds, getLoginUserId());
-        // 处理商机状态类型名称回显
-        Set<Long> statusTypeIds = pageResult.getList().stream()
-                .map(CrmBusinessDO::getStatusTypeId).filter(Objects::nonNull).collect(Collectors.toSet());
-        CrmBusinessStatusTypeQueryVO queryStatusTypeVO = new CrmBusinessStatusTypeQueryVO();
-        queryStatusTypeVO.setIdList(statusTypeIds);
-        List<CrmBusinessStatusTypeDO> statusTypeList = businessStatusTypeService.selectList(queryStatusTypeVO);
-        // 处理商机状态名称回显
-        Set<Long> statusIds = pageResult.getList().stream()
-                .map(CrmBusinessDO::getStatusId).filter(Objects::nonNull).collect(Collectors.toSet());
-        CrmBusinessStatusQueryVO queryVO = new CrmBusinessStatusQueryVO();
-        queryVO.setIdList(statusIds);
-        List<CrmBusinessStatusDO> statusList = businessStatusService.selectList(queryVO);
-        return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult, customerList, statusTypeList, statusList));
+        return success(buildBusinessDetailPageResult(pageResult));
     }
 
     @GetMapping("/page-by-customer")
@@ -123,24 +97,15 @@ public class CrmBusinessController {
     public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPageByCustomer(@Valid CrmBusinessPageReqVO pageReqVO) {
         Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPageByCustomerId(pageReqVO);
-        // 处理客户名称回显
-        // TODO @ljlleo:可以使用 CollectionUtils.convertSet 替代常用的 stream 操作,更简洁一点;下面几个也是哈;
-        Set<Long> customerIds = pageResult.getList().stream()
-                .map(CrmBusinessDO::getCustomerId).filter(Objects::nonNull).collect(Collectors.toSet());
-        List<CrmCustomerDO> customerList = customerService.getCustomerList(customerIds, getLoginUserId());
-        // 处理商机状态类型名称回显
-        Set<Long> statusTypeIds = pageResult.getList().stream()
-                .map(CrmBusinessDO::getStatusTypeId).filter(Objects::nonNull).collect(Collectors.toSet());
-        CrmBusinessStatusTypeQueryVO queryStatusTypeVO = new CrmBusinessStatusTypeQueryVO();
-        queryStatusTypeVO.setIdList(statusTypeIds);
-        List<CrmBusinessStatusTypeDO> statusTypeList = businessStatusTypeService.selectList(queryStatusTypeVO);
-        // 处理商机状态名称回显
-        Set<Long> statusIds = pageResult.getList().stream()
-                .map(CrmBusinessDO::getStatusId).filter(Objects::nonNull).collect(Collectors.toSet());
-        CrmBusinessStatusQueryVO queryVO = new CrmBusinessStatusQueryVO();
-        queryVO.setIdList(statusIds);
-        List<CrmBusinessStatusDO> statusList = businessStatusService.selectList(queryVO);
-        return success(CrmBusinessConvert.INSTANCE.convertPage(pageResult, customerList, statusTypeList, statusList));
+        return success(buildBusinessDetailPageResult(pageResult));
+    }
+
+    @GetMapping("/page-by-contact")
+    @Operation(summary = "获得联系人的商机分页")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessContactPage(@Valid CrmBusinessPageReqVO pageReqVO) {
+        PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPageByContact(pageReqVO);
+        return success(buildBusinessDetailPageResult(pageResult));
     }
 
     @GetMapping("/export-excel")
@@ -152,8 +117,27 @@ public class CrmBusinessController {
         exportReqVO.setPageSize(PAGE_SIZE_NONE);
         PageResult<CrmBusinessDO> pageResult = businessService.getBusinessPage(exportReqVO, getLoginUserId());
         // 导出 Excel
-        ExcelUtils.write(response, "商机.xls", "数据", CrmBusinessExcelVO.class,
-                CrmBusinessConvert.INSTANCE.convertList02(pageResult.getList()));
+        ExcelUtils.write(response, "商机.xls", "数据", CrmBusinessRespVO.class,
+                buildBusinessDetailPageResult(pageResult).getList());
+    }
+
+    /**
+     * 构建详细的商机分页结果
+     *
+     * @param pageResult 简单的商机分页结果
+     * @return 详细的商机分页结果
+     */
+    private PageResult<CrmBusinessRespVO> buildBusinessDetailPageResult(PageResult<CrmBusinessDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        List<CrmBusinessStatusTypeDO> statusTypeList = businessStatusTypeService.getBusinessStatusTypeList(
+                convertSet(pageResult.getList(), CrmBusinessDO::getStatusTypeId));
+        List<CrmBusinessStatusDO> statusList = businessStatusService.getBusinessStatusList(
+                convertSet(pageResult.getList(), CrmBusinessDO::getStatusId));
+        List<CrmCustomerDO> customerList = customerService.getCustomerList(
+                convertSet(pageResult.getList(), CrmBusinessDO::getCustomerId));
+        return CrmBusinessConvert.INSTANCE.convertPage(pageResult, customerList, statusTypeList, statusList);
     }
 
     @PutMapping("/transfer")

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

@@ -20,6 +20,9 @@ public class CrmBusinessPageReqVO extends PageParam {
     @Schema(description = "客户编号", example = "10795")
     private Long customerId;
 
+    @Schema(description = "联系人编号", example = "10795")
+    private Long contactId;
+
     @Schema(description = "场景类型", example = "1")
     @InEnum(CrmSceneTypeEnum.class)
     private Integer sceneType; // 场景类型,为 null 时则表示全部

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

@@ -6,6 +6,7 @@ import cn.hutool.core.util.NumberUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
@@ -13,6 +14,7 @@ import cn.iocoder.yudao.module.crm.convert.contact.CrmContactConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants;
+import cn.iocoder.yudao.module.crm.service.contact.CrmContactBusinessService;
 import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -54,10 +56,13 @@ public class CrmContactController {
     private CrmContactService contactService;
     @Resource
     private CrmCustomerService customerService;
-
     @Resource
     private AdminUserApi adminUserApi;
 
+    @Resource
+    private CrmContactBusinessService contactBusinessLinkService;
+
+    // TODO @zyna:CrmContactCreateReqVO、CrmContactUpdateReqVO、CrmContactRespVO 按照新的 VO 规范搞哈;可以参考 dept 模块
     @PostMapping("/create")
     @Operation(summary = "创建联系人")
     @PreAuthorize("@ss.hasPermission('crm:contact:create')")
@@ -96,7 +101,7 @@ public class CrmContactController {
                 NumberUtil.parseLong(contact.getCreator()), contact.getOwnerUserId())));
         // 2. 获取客户信息
         List<CrmCustomerDO> customerList = customerService.getCustomerList(
-                Collections.singletonList(contact.getCustomerId()), getLoginUserId());
+                Collections.singletonList(contact.getCustomerId()));
         // 3. 直属上级
         List<CrmContactDO> parentContactList = contactService.getContactList(
                 Collections.singletonList(contact.getParentId()), getLoginUserId());
@@ -107,10 +112,11 @@ public class CrmContactController {
     @Operation(summary = "获得联系人列表")
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<List<CrmContactSimpleRespVO>> getSimpleContactList() {
+        // TODO @zyna:建议 contactService 单独搞个 list 接口哈
         CrmContactPageReqVO pageReqVO = new CrmContactPageReqVO();
         pageReqVO.setPageSize(PAGE_SIZE_NONE);
         List<CrmContactDO> list = contactService.getContactPage(pageReqVO, getLoginUserId()).getList();
-        return success(CrmContactConvert.INSTANCE.convertAllList(list));
+        return success(BeanUtils.toBean(list, CrmContactSimpleRespVO.class));
     }
 
     @GetMapping("/page")
@@ -118,7 +124,7 @@ public class CrmContactController {
     @PreAuthorize("@ss.hasPermission('crm:contact:query')")
     public CommonResult<PageResult<CrmContactRespVO>> getContactPage(@Valid CrmContactPageReqVO pageVO) {
         PageResult<CrmContactDO> pageResult = contactService.getContactPage(pageVO, getLoginUserId());
-        return success(convertDetailContactPage(pageResult));
+        return success(buildContactDetailPage(pageResult));
     }
 
     @GetMapping("/page-by-customer")
@@ -126,7 +132,7 @@ public class CrmContactController {
     public CommonResult<PageResult<CrmContactRespVO>> getContactPageByCustomer(@Valid CrmContactPageReqVO pageVO) {
         Assert.notNull(pageVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmContactDO> pageResult = contactService.getContactPageByCustomerId(pageVO);
-        return success(convertDetailContactPage(pageResult));
+        return success(buildContactDetailPage(pageResult));
     }
 
     @GetMapping("/export-excel")
@@ -138,23 +144,23 @@ public class CrmContactController {
         exportReqVO.setPageNo(PAGE_SIZE_NONE);
         PageResult<CrmContactDO> pageResult = contactService.getContactPage(exportReqVO, getLoginUserId());
         ExcelUtils.write(response, "联系人.xls", "数据", CrmContactRespVO.class,
-                convertDetailContactPage(pageResult).getList());
+                buildContactDetailPage(pageResult).getList());
     }
 
     /**
-     * 转换成详细的联系人分页,即读取关联信息
+     * 构建详细的联系人分页结果
      *
-     * @param pageResult 联系人分页
-     * @return 详细的联系人分页
+     * @param pageResult 简单的联系人分页结果
+     * @return 详细的联系人分页结果
      */
-    private PageResult<CrmContactRespVO> convertDetailContactPage(PageResult<CrmContactDO> pageResult) {
+    private PageResult<CrmContactRespVO> buildContactDetailPage(PageResult<CrmContactDO> pageResult) {
         List<CrmContactDO> contactList = pageResult.getList();
         if (CollUtil.isEmpty(contactList)) {
             return PageResult.empty(pageResult.getTotal());
         }
         // 1. 获取客户列表
         List<CrmCustomerDO> crmCustomerDOList = customerService.getCustomerList(
-                convertSet(contactList, CrmContactDO::getCustomerId), getLoginUserId());
+                convertSet(contactList, CrmContactDO::getCustomerId));
         // 2. 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(contactList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
@@ -172,4 +178,22 @@ public class CrmContactController {
         return success(true);
     }
 
+    // ================== 关联/取关联系人  ===================
+
+    @PostMapping("/create-business-list")
+    @Operation(summary = "创建联系人与联系人的关联")
+    @PreAuthorize("@ss.hasPermission('crm:contact:create-business')")
+    public CommonResult<Boolean> createContactBusinessList(@Valid @RequestBody CrmContactBusinessReqVO createReqVO) {
+        contactBusinessLinkService.createContactBusinessList(createReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete-business-list")
+    @Operation(summary = "删除联系人与联系人的关联")
+    @PreAuthorize("@ss.hasPermission('crm:contact:delete-business')")
+    public CommonResult<Boolean> deleteContactBusinessList(@Valid @RequestBody CrmContactBusinessReqVO deleteReqVO) {
+        contactBusinessLinkService.deleteContactBusinessList(deleteReqVO);
+        return success(true);
+    }
+
 }

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

@@ -8,11 +8,11 @@ import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
 import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
-import jakarta.validation.constraints.Email;
-import jakarta.validation.constraints.NotNull;
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@@ -75,29 +75,29 @@ public class CrmContactBaseVO {
     @ExcelProperty(value = "邮箱",order = 4)
     private String email;
 
+    @Schema(description = "地区编号", example = "20158")
+    private Integer areaId;
+
     @ExcelProperty(value = "地址",order = 5)
     @Schema(description = "地址")
-    private String address;
-
-    @Schema(description = "下次联系时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
-    @ExcelProperty(value = "下次联系时间",order = 6)
-    private LocalDateTime nextTime;
+    private String detailAddress;
 
     @Schema(description = "备注", example = "你说的对")
     @ExcelProperty(value = "备注",order = 6)
     private String remark;
 
-    @Schema(description = "最后跟进时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    @ExcelProperty(value = "最后跟进时间",order = 6)
-    private LocalDateTime lastTime;
-
     @Schema(description = "负责人用户编号", example = "14334")
     @NotNull(message = "负责人不能为空")
     private Long ownerUserId;
 
-    @Schema(description = "地区编号", example = "20158")
-    private Integer areaId;
+    @Schema(description = "最后跟进时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ExcelProperty(value = "最后跟进时间",order = 6)
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "下次联系时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+    @ExcelProperty(value = "下次联系时间",order = 6)
+    private LocalDateTime contactNextTime;
 
 }

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

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

+ 0 - 115
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contactbusinesslink/CrmContactBusinessLinkController.java

@@ -1,115 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink;
-
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo.CrmContactBusinessLinkPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo.CrmContactBusinessLinkRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo.CrmContactBusinessLinkSaveReqVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contactbusinesslink.CrmContactBusinessLinkDO;
-import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
-import cn.iocoder.yudao.module.crm.service.contactbusinesslink.CrmContactBusinessLinkService;
-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.transaction.annotation.Transactional;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.*;
-
-import jakarta.annotation.Resource;
-import jakarta.validation.Valid;
-import java.util.List;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
-
-@Tag(name = "管理后台 - CRM 联系人商机关联")
-@RestController
-@RequestMapping("/crm/contact-business-link")
-@Validated
-public class CrmContactBusinessLinkController {
-
-    @Resource
-    private CrmContactBusinessLinkService contactBusinessLinkService;
-    @Resource
-    private CrmBusinessService crmBusinessService;
-
-    // TODO @zyna:createContactBusinessLink 和 createContactBusinessLinkBatch 是不是合并成一个接口?contactId、List<businessId>
-    @PostMapping("/create")
-    @Operation(summary = "创建联系人商机关联")
-    @PreAuthorize("@ss.hasPermission('crm:contact-business-link:create')")
-    public CommonResult<Long> createContactBusinessLink(@Valid @RequestBody CrmContactBusinessLinkSaveReqVO createReqVO) {
-        return success(contactBusinessLinkService.createContactBusinessLink(createReqVO));
-    }
-
-    @PostMapping("/create-batch")
-    @Operation(summary = "创建联系人商机关联")
-    @PreAuthorize("@ss.hasPermission('crm:contact-business-link:create')")
-    @Transactional(rollbackFor = Exception.class)
-    public CommonResult<Boolean> createContactBusinessLinkBatch(
-            @Valid @RequestBody List<CrmContactBusinessLinkSaveReqVO> createReqVO) {
-        createReqVO.stream().forEach(item -> {
-            CrmBusinessDO crmBusinessDO = crmBusinessService.getBusiness(item.getBusinessId());
-            if(crmBusinessDO == null){
-                throw exception(BUSINESS_NOT_EXISTS);
-            }
-        });
-        contactBusinessLinkService.createContactBusinessLinkBatch(createReqVO);
-        return success(true);
-    }
-
-    // TODO @zyna:这个接口是不是可以删除掉了哈?应该不存在更新。
-    @PutMapping("/update")
-    @Operation(summary = "更新联系人商机关联")
-    @PreAuthorize("@ss.hasPermission('crm:contact-business-link:update')")
-    public CommonResult<Boolean> updateContactBusinessLink(@Valid @RequestBody CrmContactBusinessLinkSaveReqVO updateReqVO) {
-        contactBusinessLinkService.updateContactBusinessLink(updateReqVO);
-        return success(true);
-    }
-
-    // TODO @zyna:删除,是不是传递 ids?
-    @DeleteMapping("/delete-batch")
-    @Operation(summary = "批量删除联系人商机关联")
-    @PreAuthorize("@ss.hasPermission('crm:contact-business-link:delete')")
-    public CommonResult<Boolean> deleteContactBusinessLinkBatch(@Valid @RequestBody List<CrmContactBusinessLinkSaveReqVO> deleteList) {
-        contactBusinessLinkService.deleteContactBusinessLink(deleteList);
-        return success(true);
-    }
-
-    // TODO @zyna:这个接口是不是可以删除掉了哈?应该不存在单个读取;
-    @GetMapping("/get")
-    @Operation(summary = "获得联系人商机关联")
-    @Parameter(name = "id", description = "编号", required = true, example = "1024")
-    @PreAuthorize("@ss.hasPermission('crm:contact-business-link:query')")
-    public CommonResult<CrmContactBusinessLinkRespVO> getContactBusinessLink(@RequestParam("id") Long id) {
-        CrmContactBusinessLinkDO contactBusinessLink = contactBusinessLinkService.getContactBusinessLink(id);
-        return success(BeanUtils.toBean(contactBusinessLink, CrmContactBusinessLinkRespVO.class));
-    }
-
-    // TODO @zyna:这个可以转化下,使用客户编号去查询,就是使用 CrmBusinessController 的 getBusinessPageByCustomer 接口;目的是:复用
-    @GetMapping("/page-by-contact")
-    @Operation(summary = "获得联系人商机关联")
-    @PreAuthorize("@ss.hasPermission('crm:contact-business-link:query')")
-    public CommonResult<PageResult<CrmBusinessRespVO>> getContactBusinessLinkByContact(
-            @Valid CrmContactBusinessLinkPageReqVO pageReqVO) {
-        PageResult<CrmBusinessRespVO> contactBusinessLink = contactBusinessLinkService.getContactBusinessLinkPageByContact(pageReqVO);
-        return success(contactBusinessLink);
-    }
-
-    // TODO @zyna:这个优化下,搞到 CrmBusinessController 里去,加一个 CrmBusinessController 的 getBusinessPageByContact 接口;目的是:
-    @GetMapping("/page")
-    @Operation(summary = "获得联系人商机关联分页")
-    @PreAuthorize("@ss.hasPermission('crm:contact-business-link:query')")
-    public CommonResult<PageResult<CrmContactBusinessLinkRespVO>> getContactBusinessLinkPage(
-            @Valid CrmContactBusinessLinkPageReqVO pageReqVO) {
-        PageResult<CrmContactBusinessLinkDO> pageResult = contactBusinessLinkService.getContactBusinessLinkPage(pageReqVO);
-        return success(BeanUtils.toBean(pageResult, CrmContactBusinessLinkRespVO.class));
-    }
-
-    // TODO @zyna:最终梳理完后,应该就 2 个接口,要不直接合并到 CrmContactController 中,不作为独立模块,就关联、接触关联。其实和 user 设置它有哪些岗位、部门是类似的。
-
-}

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

@@ -1,30 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.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 CrmContactBusinessLinkPageReqVO extends PageParam {
-
-    @Schema(description = "联系人编号", example = "20878")
-    private Long contactId;
-
-    @Schema(description = "商机编号", example = "7638")
-    private Long businessId;
-
-    @Schema(description = "创建时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime[] createTime;
-
-}

+ 0 - 26
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contactbusinesslink/vo/CrmContactBusinessLinkRespVO.java

@@ -1,26 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo;
-
-import com.alibaba.excel.annotation.ExcelProperty;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import java.time.LocalDateTime;
-
-@Schema(description = "管理后台 - CRM 联系人商机关联 Response VO")
-@Data
-public class CrmContactBusinessLinkRespVO {
-
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "17220")
-    @ExcelProperty("主键")
-    private Long id;
-
-    @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20878")
-    private Long contactId;
-
-    @Schema(description = "商机编号", requiredMode =  Schema.RequiredMode.REQUIRED, example = "7638")
-    private Long businessId;
-
-    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
-    private LocalDateTime createTime;
-
-}

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

@@ -1,23 +0,0 @@
-package cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import jakarta.validation.constraints.NotNull;
-
-@Schema(description = "管理后台 - CRM 联系人商机关联新增/修改 Request VO")
-@Data
-public class CrmContactBusinessLinkSaveReqVO {
-
-    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "17220")
-    private Long id;
-
-    @Schema(description = "联系人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20878")
-    @NotNull(message="联系人不能为空")
-    private Long contactId;
-
-    @Schema(description = "商机编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7638")
-    @NotNull(message="商机不能为空")
-    private Long businessId;
-
-}

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

@@ -88,7 +88,7 @@ public class CrmContractController {
     @PreAuthorize("@ss.hasPermission('crm:contract:query')")
     public CommonResult<PageResult<ContractRespVO>> getContractPage(@Valid CrmContractPageReqVO pageVO) {
         PageResult<CrmContractDO> pageResult = contractService.getContractPage(pageVO, getLoginUserId());
-        return success(convertDetailContractPage(pageResult));
+        return success(buildContractDetailPage(pageResult));
     }
 
     @GetMapping("/page-by-customer")
@@ -96,7 +96,7 @@ public class CrmContractController {
     public CommonResult<PageResult<ContractRespVO>> getContractPageByCustomer(@Valid CrmContractPageReqVO pageVO) {
         Assert.notNull(pageVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmContractDO> pageResult = contractService.getContractPageByCustomerId(pageVO);
-        return success(convertDetailContractPage(pageResult));
+        return success(buildContractDetailPage(pageResult));
     }
 
     @GetMapping("/export-excel")
@@ -112,19 +112,19 @@ public class CrmContractController {
     }
 
     /**
-     * 转换成详细的合同分页,即读取关联信息
+     * 构建详细的合同分页结果
      *
-     * @param pageResult 合同分页
-     * @return 详细的合同分页
+     * @param pageResult 简单的合同分页结果
+     * @return 详细的合同分页结果
      */
-    private PageResult<ContractRespVO> convertDetailContractPage(PageResult<CrmContractDO> pageResult) {
+    private PageResult<ContractRespVO> buildContractDetailPage(PageResult<CrmContractDO> pageResult) {
         List<CrmContractDO> contactList = pageResult.getList();
         if (CollUtil.isEmpty(contactList)) {
             return PageResult.empty(pageResult.getTotal());
         }
         // 1. 获取客户列表
         List<CrmCustomerDO> customerList = customerService.getCustomerList(
-                convertSet(contactList, CrmContractDO::getCustomerId), getLoginUserId());
+                convertSet(contactList, CrmContractDO::getCustomerId));
         // 2. 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(contactList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));

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

@@ -1,8 +1,8 @@
 ### 请求 /transfer
 PUT {{baseUrl}}/crm/customer/transfer
-Content-Type: application/json
+Content-Type: application/-id: {{adminTenentId}}json
 Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}
+tenant
 
 {
   "id": 10,

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

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.*;
@@ -12,6 +13,7 @@ import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.logger.OperateLogApi;
+import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2PageReqDTO;
 import cn.iocoder.yudao.module.system.api.logger.dto.OperateLogV2RespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -56,15 +58,18 @@ public class CrmCustomerController {
     @Resource
     private OperateLogApi operateLogApi;
 
+    // TODO @puhui999:把 CrmCustomerCreateReqVO、CrmCustomerUpdateReqVO、CrmCustomerRespVO 按照新的规范,搞一下哈;
     @PostMapping("/create")
     @Operation(summary = "创建客户")
+    @OperateLog(enable = false) // TODO 关闭原有日志记录;@puhui999:注解都先删除。先记录,没关系。我们下个迭代,就都删除掉操作日志了;
     @PreAuthorize("@ss.hasPermission('crm:customer:create')")
     public CommonResult<Long> createCustomer(@Valid @RequestBody CrmCustomerCreateReqVO createReqVO) {
         return success(customerService.createCustomer(createReqVO, getLoginUserId()));
     }
 
     @PutMapping("/update")
-    //@Operation(summary = "更新客户")
+    @Operation(summary = "更新客户")
+    @OperateLog(enable = false) // TODO 关闭原有日志记录
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")
     public CommonResult<Boolean> updateCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
         customerService.updateCustomer(updateReqVO);
@@ -73,6 +78,7 @@ public class CrmCustomerController {
 
     @DeleteMapping("/delete")
     @Operation(summary = "删除客户")
+    @OperateLog(enable = false) // TODO 关闭原有日志记录
     @Parameter(name = "id", description = "编号", required = true)
     @PreAuthorize("@ss.hasPermission('crm:customer:delete')")
     public CommonResult<Boolean> deleteCustomer(@RequestParam("id") Long id) {
@@ -97,6 +103,7 @@ public class CrmCustomerController {
         return success(CrmCustomerConvert.INSTANCE.convert(customer, userMap, deptMap));
     }
 
+    // TODO @puhui999:这个查询会查出多个;微信发你图了
     @GetMapping("/page")
     @Operation(summary = "获得客户分页")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
@@ -108,6 +115,7 @@ public class CrmCustomerController {
         }
 
         // 2. 拼接数据
+        // TODO @puhui999:距离进入公海的时间
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                 convertSetByFlatMap(pageResult.getList(), user -> Stream.of(Long.parseLong(user.getCreator()), user.getOwnerUserId())));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
@@ -128,38 +136,30 @@ public class CrmCustomerController {
     }
 
     @PutMapping("/transfer")
-    //@Operation(summary = "客户转移")
+    @Operation(summary = "转移客户")
+    @OperateLog(enable = false) // TODO 关闭原有日志记录
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")
     public CommonResult<Boolean> transfer(@Valid @RequestBody CrmCustomerTransferReqVO reqVO) {
         customerService.transferCustomer(reqVO, getLoginUserId());
         return success(true);
     }
 
-    // TODO @puhui999:operate-log-list 或者 operate-log-page 如果分页
-    @GetMapping("/operate-log")
+    // TODO @puhui999:是不是接口只要传递 bizId,由 Controller 自己组装出 OperateLogV2PageReqDTO
+    @GetMapping("/operate-log-page")
     @Operation(summary = "获得客户操作日志")
-    @Parameter(name = "id", description = "编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:customer:query')")
-    // TODO @puhui999:最好有读权限;方法名改成 getCustomerOperateLog
-    public CommonResult<List<OperateLogV2RespDTO>> getOperateLog(@RequestParam("id") Long id) {
-        // 1. 获取客户
-        // TODO @puhui999:这个校验可以去掉哈;
-        CrmCustomerDO customer = customerService.getCustomer(id);
-        if (customer == null) {
-            return success(null);
-        }
-
-        // 2. 获取操作日志
-        // TODO @puhui999:操作日志,返回可能要分页哈;
-        return success(operateLogApi.getOperateLogByModuleAndBizId(CRM_CUSTOMER, id));
+    public CommonResult<PageResult<OperateLogV2RespDTO>> getCustomerOperateLog(CrmCustomerOperateLogPageReqVO reqVO) {
+        reqVO.setPageSize(PAGE_SIZE_NONE); // 不分页
+        reqVO.setBizType(CRM_CUSTOMER);
+        return success(operateLogApi.getOperateLogPage(BeanUtils.toBean(reqVO, OperateLogV2PageReqDTO.class)));
     }
 
-    // TODO @Joey:单独建一个属于自己业务的 ReqVO;因为前端如果模拟请求,是不是可以更新其它字段了;
     @PutMapping("/lock")
     @Operation(summary = "锁定/解锁客户")
+    @OperateLog(enable = false) // TODO 关闭原有日志记录
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")
-    public CommonResult<Boolean> lockCustomer(@Valid @RequestBody CrmCustomerUpdateReqVO updateReqVO) {
-        customerService.lockCustomer(updateReqVO);
+    public CommonResult<Boolean> lockCustomer(@Valid @RequestBody CrmCustomerLockReqVO lockReqVO) {
+        customerService.lockCustomer(lockReqVO, getLoginUserId());
         return success(true);
     }
 
@@ -167,6 +167,7 @@ public class CrmCustomerController {
 
     @PutMapping("/put-pool")
     @Operation(summary = "数据放入公海")
+    @OperateLog(enable = false) // TODO 关闭原有日志记录
     @Parameter(name = "id", description = "客户编号", required = true, example = "1024")
     @PreAuthorize("@ss.hasPermission('crm:customer:update')")
     public CommonResult<Boolean> putCustomerPool(@RequestParam("id") Long id) {
@@ -183,6 +184,7 @@ public class CrmCustomerController {
         return success(true);
     }
 
+    // TODO @puhui999:需要搞个 VO 类
     @PutMapping("/distribute")
     @Operation(summary = "分配公海给对应负责人")
     @Parameters({
@@ -192,7 +194,6 @@ public class CrmCustomerController {
     @PreAuthorize("@ss.hasPermission('crm:customer:distribute')")
     public CommonResult<Boolean> distributeCustomer(@RequestParam(value = "ids") List<Long> ids,
                                                     @RequestParam(value = "ownerUserId") Long ownerUserId) {
-        // 领取公海数据
         customerService.receiveCustomer(ids, ownerUserId);
         return success(true);
     }

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

@@ -43,6 +43,7 @@ public class CrmCustomerLimitConfigController {
     @Resource
     private AdminUserApi adminUserApi;
 
+    // TODO @puhui999:可以把 vo 改下哈
     @PostMapping("/create")
     @Operation(summary = "创建客户限制配置")
     @PreAuthorize("@ss.hasPermission('crm:customer-limit-config:create')")

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

@@ -26,6 +26,7 @@ public class CrmCustomerPoolConfigController {
     @Resource
     private CrmCustomerPoolConfigService customerPoolConfigService;
 
+    // TODO @puhui999:可以把 vo 改下哈
     @GetMapping("/get")
     @Operation(summary = "获取客户公海规则设置")
     @PreAuthorize("@ss.hasPermission('crm:customer-pool-config:query')")

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

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户锁定/解锁 Request VO")
+@Data
+public class CrmCustomerLockReqVO {
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13563")
+    private Long id;
+
+    @Schema(description = "客户锁定状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+    private Boolean lockStatus;
+
+}

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

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.controller.admin.customer.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - crm 客户操作日志分页 Request VO")
+@Data
+public class CrmCustomerOperateLogPageReqVO extends PageParam {
+
+    @Schema(description = "模块数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long bizId;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long userId;
+
+    @Schema(description = "模块类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private String bizType;
+
+}

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

@@ -28,4 +28,6 @@ public class CrmCustomerTransferReqVO {
     @Schema(description = "老负责人加入团队后的权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
     private Integer oldOwnerPermissionLevel;
 
+    // TODO @puhui999:联系人、商机、合同的转移
+
 }

+ 3 - 3
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/poolconfig/CrmCustomerPoolConfigSaveReqVO.java

@@ -16,9 +16,9 @@ import java.util.Objects;
 @ToString(callSuper = true)
 public class CrmCustomerPoolConfigSaveReqVO extends CrmCustomerPoolConfigBaseVO {
 
-    // TODO @wanwan:AssertTrue 必须 is 开头哈;注意需要 json 忽略下,避免被序列化;
+    // TODO @puhui999:AssertTrue 必须 is 开头哈;注意需要 json 忽略下,避免被序列化;
     @AssertTrue(message = "客户公海规则设置不正确")
-    // TODO @wanwan:这个方法,是不是拆成 2 个,一个校验 contactExpireDays、一个校验 dealExpireDays;
+    // TODO @puhui999:这个方法,是不是拆成 2 个,一个校验 contactExpireDays、一个校验 dealExpireDays;
     public boolean poolEnableValid() {
         if (!BooleanUtil.isTrue(getEnabled())) {
             return true;
@@ -27,7 +27,7 @@ public class CrmCustomerPoolConfigSaveReqVO extends CrmCustomerPoolConfigBaseVO
     }
 
     @AssertTrue(message = "客户公海规则设置不正确")
-    // TODO @wanwan:这个方法,是不是改成 isNotifyDaysValid() 更好点?本质校验的是 notifyDays 是否为空
+    // TODO @puhui999:这个方法,是不是改成 isNotifyDaysValid() 更好点?本质校验的是 notifyDays 是否为空
     public boolean notifyEnableValid() {
         if (!BooleanUtil.isTrue(getNotifyEnabled())) {
             return true;

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

@@ -8,8 +8,8 @@ import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionR
 import cn.iocoder.yudao.module.crm.controller.admin.permission.vo.CrmPermissionUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.permission.CrmPermissionConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
+import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.PostApi;
@@ -21,12 +21,12 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.Parameters;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import jakarta.annotation.Resource;
-import jakarta.validation.Valid;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -103,6 +103,7 @@ public class CrmPermissionController {
         // 拼接数据
         List<AdminUserRespDTO> userList = adminUserApi.getUserList(convertSet(permission, CrmPermissionDO::getUserId));
         Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userList, AdminUserRespDTO::getDeptId));
+        // TODO @puhui999:可能 postIds 为空的时候,会导致报错,看看怎么 fix 下
         Set<Long> postIds = CollectionUtils.convertSetByFlatMap(userList, AdminUserRespDTO::getPostIds, Collection::stream);
         Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
         return success(CrmPermissionConvert.INSTANCE.convert(permission, userList, deptMap, postMap));

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

@@ -93,7 +93,7 @@ public class CrmReceivableController {
     @PreAuthorize("@ss.hasPermission('crm:receivable:query')")
     public CommonResult<PageResult<CrmReceivableRespVO>> getReceivablePage(@Valid CrmReceivablePageReqVO pageReqVO) {
         PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePage(pageReqVO, getLoginUserId());
-        return success(convertDetailReceivablePage(pageResult));
+        return success(buildReceivableDetailPage(pageResult));
     }
 
     @GetMapping("/page-by-customer")
@@ -101,7 +101,7 @@ public class CrmReceivableController {
     public CommonResult<PageResult<CrmReceivableRespVO>> getReceivablePageByCustomer(@Valid CrmReceivablePageReqVO pageReqVO) {
         Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
         PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePageByCustomerId(pageReqVO);
-        return success(convertDetailReceivablePage(pageResult));
+        return success(buildReceivableDetailPage(pageResult));
     }
 
     // TODO 芋艿:后面在优化导出
@@ -115,23 +115,23 @@ public class CrmReceivableController {
         PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePage(exportReqVO, getLoginUserId());
         // 导出 Excel
         ExcelUtils.write(response, "回款.xls", "数据", CrmReceivableRespVO.class,
-                convertDetailReceivablePage(pageResult).getList());
+                buildReceivableDetailPage(pageResult).getList());
     }
 
     /**
-     * 转换成详细的回款分页,即读取关联信息
+     * 构建详细的回款分页结果
      *
-     * @param pageResult 回款分页
-     * @return 详细的回款分页
+     * @param pageResult 简单的回款分页结果
+     * @return 详细的回款分页结果
      */
-    private PageResult<CrmReceivableRespVO> convertDetailReceivablePage(PageResult<CrmReceivableDO> pageResult) {
+    private PageResult<CrmReceivableRespVO> buildReceivableDetailPage(PageResult<CrmReceivableDO> pageResult) {
         List<CrmReceivableDO> receivableList = pageResult.getList();
         if (CollUtil.isEmpty(receivableList)) {
             return PageResult.empty(pageResult.getTotal());
         }
         // 1. 获取客户列表
         List<CrmCustomerDO> customerList = customerService.getCustomerList(
-                convertSet(receivableList, CrmReceivableDO::getCustomerId), getLoginUserId());
+                convertSet(receivableList, CrmReceivableDO::getCustomerId));
         // 2. 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(receivableList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));

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

@@ -123,10 +123,10 @@ public class CrmReceivablePlanController {
     }
 
     /**
-     * 转换成详细的回款计划分页,即读取关联信息
+     * 构建详细的回款计划分页结果
      *
-     * @param pageResult 回款计划分页
-     * @return 详细的回款计划分页
+     * @param pageResult 简单的回款计划分页结果
+     * @return 详细的回款计划分页结果
      */
     private PageResult<CrmReceivablePlanRespVO> convertDetailReceivablePlanPage(PageResult<CrmReceivablePlanDO> pageResult) {
         List<CrmReceivablePlanDO> receivablePlanList = pageResult.getList();
@@ -135,7 +135,7 @@ public class CrmReceivablePlanController {
         }
         // 1. 获取客户列表
         List<CrmCustomerDO> customerList = customerService.getCustomerList(
-                convertSet(receivablePlanList, CrmReceivablePlanDO::getCustomerId), getLoginUserId());
+                convertSet(receivablePlanList, CrmReceivablePlanDO::getCustomerId));
         // 2. 获取创建人、负责人列表
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(receivablePlanList,
                 contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));

+ 5 - 7
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/business/CrmBusinessConvert.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.convert.business;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
@@ -9,7 +10,6 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
-import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
@@ -34,25 +34,23 @@ public interface CrmBusinessConvert {
     CrmBusinessRespVO convert(CrmBusinessDO bean);
     List<CrmBusinessRespVO> convert(List<CrmBusinessDO> bean);
 
-    PageResult<CrmBusinessRespVO> convertPage(PageResult<CrmBusinessDO> page);
-
     List<CrmBusinessExcelVO> convertList02(List<CrmBusinessDO> list);
 
     @Mapping(target = "bizId", source = "reqVO.id")
     CrmPermissionTransferReqBO convert(CrmBusinessTransferReqVO reqVO, Long userId);
 
-    default PageResult<CrmBusinessRespVO> convertPage(PageResult<CrmBusinessDO> page, List<CrmCustomerDO> customerList,
+    default PageResult<CrmBusinessRespVO> convertPage(PageResult<CrmBusinessDO> pageResult, List<CrmCustomerDO> customerList,
                                                       List<CrmBusinessStatusTypeDO> statusTypeList, List<CrmBusinessStatusDO> statusList) {
-        PageResult<CrmBusinessRespVO> result = convertPage(page);
+        PageResult<CrmBusinessRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmBusinessRespVO.class);
         // 拼接关联字段
         Map<Long, String> customerMap = convertMap(customerList, CrmCustomerDO::getId, CrmCustomerDO::getName);
         Map<Long, String> statusTypeMap = convertMap(statusTypeList, CrmBusinessStatusTypeDO::getId, CrmBusinessStatusTypeDO::getName);
         Map<Long, String> statusMap = convertMap(statusList, CrmBusinessStatusDO::getId, CrmBusinessStatusDO::getName);
-        result.getList().forEach(type -> type
+        voPageResult.getList().forEach(type -> type
                 .setCustomerName(customerMap.get(type.getCustomerId()))
                 .setStatusTypeName(statusTypeMap.get(type.getStatusTypeId()))
                 .setStatusName(statusMap.get(type.getStatusId())));
-        return result;
+        return voPageResult;
     }
 
 }

+ 21 - 48
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contact/CrmContactConvert.java

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.crm.convert.contact;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
@@ -13,12 +15,10 @@ import org.mapstruct.factory.Mappers;
 
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 
-// TODO 芋艿:convert 后面在梳理下,略微有点乱
 /**
  * CRM 联系人 Convert
  *
@@ -39,64 +39,37 @@ public interface CrmContactConvert {
 
     PageResult<CrmContactRespVO> convertPage(PageResult<CrmContactDO> page);
 
+    @Mapping(target = "bizId", source = "reqVO.id")
+    CrmPermissionTransferReqBO convert(CrmContactTransferReqVO reqVO, Long userId);
+
     default PageResult<CrmContactRespVO> convertPage(PageResult<CrmContactDO> pageResult, Map<Long, AdminUserRespDTO> userMap,
                                                      List<CrmCustomerDO> customerList, List<CrmContactDO> parentContactList) {
-        List<CrmContactRespVO> list = converList(pageResult.getList(), userMap, customerList, parentContactList);
-        return convertPage(pageResult).setList(list);
+        PageResult<CrmContactRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmContactRespVO.class);
+        // 拼接关联字段
+        Map<Long, CrmContactDO> parentContactMap = convertMap(parentContactList, CrmContactDO::getId);
+        Map<Long, CrmCustomerDO> customerMap = convertMap(customerList, CrmCustomerDO::getId);
+        voPageResult.getList().forEach(item -> {
+            setUserInfo(item, userMap);
+            findAndThen(customerMap, item.getCustomerId(), customer -> item.setCustomerName(customer.getName()));
+            findAndThen(parentContactMap, item.getParentId(), contactDO -> item.setParentName(contactDO.getName()));
+        });
+        return voPageResult;
     }
 
-    List<CrmContactSimpleRespVO> convertAllList(List<CrmContactDO> list);
-
-    @Mapping(target = "bizId", source = "reqVO.id")
-    CrmPermissionTransferReqBO convert(CrmContactTransferReqVO reqVO, Long userId);
-
-    /**
-     * 转换详情信息
-     *
-     * @param contactDO         联系人
-     * @param userMap           用户列表
-     * @param crmCustomerDOList 客户
-     * @return ContactRespVO
-     */
-    default CrmContactRespVO convert(CrmContactDO contactDO, Map<Long, AdminUserRespDTO> userMap, List<CrmCustomerDO> crmCustomerDOList,
-                                     List<CrmContactDO> contactList) {
+    default CrmContactRespVO convert(CrmContactDO contactDO, Map<Long, AdminUserRespDTO> userMap,
+                                     List<CrmCustomerDO> customerList, List<CrmContactDO> parentContactList) {
         CrmContactRespVO contactVO = convert(contactDO);
         setUserInfo(contactVO, userMap);
-        Map<Long, CrmCustomerDO> ustomerMap = crmCustomerDOList.stream().collect(Collectors.toMap(CrmCustomerDO::getId, v -> v));
-        Map<Long, CrmContactDO> contactMap = contactList.stream().collect(Collectors.toMap(CrmContactDO::getId, v -> v));
-        findAndThen(ustomerMap, contactDO.getCustomerId(), customer -> contactVO.setCustomerName(customer.getName()));
+        Map<Long, CrmCustomerDO> customerMap = CollectionUtils.convertMap(customerList, CrmCustomerDO::getId);
+        Map<Long, CrmContactDO> contactMap = CollectionUtils.convertMap(parentContactList, CrmContactDO::getId);
+        findAndThen(customerMap, contactDO.getCustomerId(), customer -> contactVO.setCustomerName(customer.getName()));
         findAndThen(contactMap, contactDO.getParentId(), contact -> contactVO.setParentName(contact.getName()));
         return contactVO;
     }
 
-    default List<CrmContactRespVO> converList(List<CrmContactDO> contactList, Map<Long, AdminUserRespDTO> userMap,
-                                              List<CrmCustomerDO> customerList, List<CrmContactDO> parentContactList) {
-        List<CrmContactRespVO> result = convertList(contactList);
-        Map<Long, CrmContactDO> parentContactMap = convertMap(parentContactList, CrmContactDO::getId);
-        Map<Long, CrmCustomerDO> customerMap = convertMap(customerList, CrmCustomerDO::getId);
-        result.forEach(item -> {
-            setUserInfo(item, userMap);
-            findAndThen(customerMap, item.getCustomerId(), customer -> { // TODO @zyna:这里的 { 可以去掉
-                item.setCustomerName(customer.getName());
-            });
-            findAndThen(parentContactMap, item.getParentId(), contactDO -> {  // TODO @zyna:这里的 { 可以去掉
-                item.setParentName(contactDO.getName());
-            });
-        });
-        return result;
-    }
-
-    /**
-     * 设置用户信息
-     *
-     * @param contactRespVO 联系人Response VO
-     * @param userMap       用户信息 map
-     */
     static void setUserInfo(CrmContactRespVO contactRespVO, Map<Long, AdminUserRespDTO> userMap) {
         contactRespVO.setAreaName(AreaUtils.format(contactRespVO.getAreaId()));
-        findAndThen(userMap, contactRespVO.getOwnerUserId(), user -> {
-            contactRespVO.setOwnerUserName(user == null ? "" : user.getNickname());
-        });
+        findAndThen(userMap, contactRespVO.getOwnerUserId(), user -> contactRespVO.setOwnerUserName(user.getNickname()));
         findAndThen(userMap, Long.parseLong(contactRespVO.getCreator()), user -> contactRespVO.setCreatorName(user.getNickname()));
     }
 

+ 0 - 16
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contactbusinessslink/CrmContactBusinessLinkConvert.java

@@ -1,16 +0,0 @@
-package cn.iocoder.yudao.module.crm.convert.contactbusinessslink;
-
-import cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo.CrmContactBusinessLinkSaveReqVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contactbusinesslink.CrmContactBusinessLinkDO;
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-
-import java.util.List;
-
-// TODO @zyna:使用 BeanUtils 慢慢替代现有的 mapstruct 哈
-@Mapper
-public interface CrmContactBusinessLinkConvert {
-    CrmContactBusinessLinkConvert INSTANCE = Mappers.getMapper(CrmContactBusinessLinkConvert.class);
-    CrmContactBusinessLinkDO convert(CrmContactBusinessLinkSaveReqVO bean);
-    List<CrmContactBusinessLinkDO> convert(List<CrmContactBusinessLinkSaveReqVO> bean);
-}

+ 8 - 15
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/contract/CrmContractConvert.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.convert.contract;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.*;
 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
@@ -43,23 +44,15 @@ public interface CrmContractConvert {
 
     default PageResult<ContractRespVO> convertPage(PageResult<CrmContractDO> pageResult, Map<Long, AdminUserRespDTO> userMap,
                                                      List<CrmCustomerDO> customerList) {
-        return new PageResult<>(converList(pageResult.getList(), userMap, customerList), pageResult.getTotal());
-    }
-
-    default List<ContractRespVO> converList(List<CrmContractDO> contractList, Map<Long, AdminUserRespDTO> userMap,
-                                            List<CrmCustomerDO> customerList) {
-        List<ContractRespVO> result = convertList(contractList);
+        PageResult<ContractRespVO> voPageResult = BeanUtils.toBean(pageResult, ContractRespVO.class);
+        // 拼接关联字段
         Map<Long, CrmCustomerDO> customerMap = convertMap(customerList, CrmCustomerDO::getId);
-        result.forEach(item -> {
-            setUserInfo(item, userMap);
-            findAndThen(customerMap, item.getCustomerId(), customer -> item.setCustomerName(customer.getName()));
+        voPageResult.getList().forEach(contract -> {
+            findAndThen(userMap, contract.getOwnerUserId(), user -> contract.setOwnerUserName(user.getNickname()));
+            findAndThen(userMap, Long.parseLong(contract.getCreator()), user -> contract.setCreatorName(user.getNickname()));
+            findAndThen(customerMap, contract.getCustomerId(), customer -> contract.setCustomerName(customer.getName()));
         });
-        return result;
-    }
-
-    static void setUserInfo(ContractRespVO contract, Map<Long, AdminUserRespDTO> userMap) {
-        findAndThen(userMap, contract.getOwnerUserId(), user -> contract.setOwnerUserName(user.getNickname()));
-        findAndThen(userMap, Long.parseLong(contract.getCreator()), user -> contract.setCreatorName(user.getNickname()));
+        return voPageResult;
     }
 
 }

+ 0 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/customer/CrmCustomerConvert.java

@@ -12,7 +12,6 @@ import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
-import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;

+ 9 - 18
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivableConvert.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.convert.receivable;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableTransferReqVO;
@@ -36,29 +37,19 @@ public interface CrmReceivableConvert {
 
     CrmReceivableRespVO convert(CrmReceivableDO bean);
 
-    List<CrmReceivableRespVO> convertList(List<CrmReceivableDO> list);
-
     default PageResult<CrmReceivableRespVO> convertPage(PageResult<CrmReceivableDO> pageResult, Map<Long, AdminUserRespDTO> userMap,
                                                         List<CrmCustomerDO> customerList, List<CrmContractDO> contractList) {
-        return new PageResult<>(converList(pageResult.getList(), userMap, customerList, contractList), pageResult.getTotal());
-    }
-
-    default List<CrmReceivableRespVO> converList(List<CrmReceivableDO> receivableList, Map<Long, AdminUserRespDTO> userMap,
-                                                 List<CrmCustomerDO> customerList, List<CrmContractDO> contractList) {
-        List<CrmReceivableRespVO> result = convertList(receivableList);
+        PageResult<CrmReceivableRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmReceivableRespVO.class);
+        // 拼接关联字段
         Map<Long, CrmCustomerDO> customerMap = convertMap(customerList, CrmCustomerDO::getId);
         Map<Long, CrmContractDO> contractMap = convertMap(contractList, CrmContractDO::getId);
-        result.forEach(item -> {
-            setUserInfo(item, userMap);
-            findAndThen(customerMap, item.getCustomerId(), customer -> item.setCustomerName(customer.getName()));
-            findAndThen(contractMap, item.getContractId(), contract -> item.setContractNo(contract.getNo()));
+        voPageResult.getList().forEach(receivable -> {
+            findAndThen(userMap, receivable.getOwnerUserId(), user -> receivable.setOwnerUserName(user.getNickname()));
+            findAndThen(userMap, Long.parseLong(receivable.getCreator()), user -> receivable.setCreatorName(user.getNickname()));
+            findAndThen(customerMap, receivable.getCustomerId(), customer -> receivable.setCustomerName(customer.getName()));
+            findAndThen(contractMap, receivable.getContractId(), contract -> receivable.setContractNo(contract.getNo()));
         });
-        return result;
-    }
-
-    static void setUserInfo(CrmReceivableRespVO receivable, Map<Long, AdminUserRespDTO> userMap) {
-        findAndThen(userMap, receivable.getOwnerUserId(), user -> receivable.setOwnerUserName(user.getNickname()));
-        findAndThen(userMap, Long.parseLong(receivable.getCreator()), user -> receivable.setCreatorName(user.getNickname()));
+        return voPageResult;
     }
 
     @Mapping(target = "bizId", source = "reqVO.id")

+ 10 - 20
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/convert/receivable/CrmReceivablePlanConvert.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.convert.receivable;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanCreateReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanRespVO;
 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanTransferReqVO;
@@ -37,33 +38,22 @@ public interface CrmReceivablePlanConvert {
 
     CrmReceivablePlanRespVO convert(CrmReceivablePlanDO bean);
 
-    List<CrmReceivablePlanRespVO> convertList(List<CrmReceivablePlanDO> list);
-
     default PageResult<CrmReceivablePlanRespVO> convertPage(PageResult<CrmReceivablePlanDO> pageResult, Map<Long, AdminUserRespDTO> userMap,
                                                             List<CrmCustomerDO> customerList, List<CrmContractDO> contractList,
                                                             List<CrmReceivableDO> receivableList) {
-        return new PageResult<>(converList(pageResult.getList(), userMap, customerList, contractList, receivableList), pageResult.getTotal());
-    }
-
-    default List<CrmReceivablePlanRespVO> converList(List<CrmReceivablePlanDO> receivablePlanList, Map<Long, AdminUserRespDTO> userMap,
-                                                     List<CrmCustomerDO> customerList, List<CrmContractDO> contractList,
-                                                     List<CrmReceivableDO> receivableList) {
-        List<CrmReceivablePlanRespVO> result = convertList(receivablePlanList);
+        PageResult<CrmReceivablePlanRespVO> voPageResult = BeanUtils.toBean(pageResult, CrmReceivablePlanRespVO.class);
+        // 拼接关联字段
         Map<Long, CrmCustomerDO> customerMap = convertMap(customerList, CrmCustomerDO::getId);
         Map<Long, CrmContractDO> contractMap = convertMap(contractList, CrmContractDO::getId);
         Map<Long, CrmReceivableDO> receivableMap = convertMap(receivableList, CrmReceivableDO::getId);
-        result.forEach(item -> {
-            setUserInfo(item, userMap);
-            findAndThen(customerMap, item.getCustomerId(), customer -> item.setCustomerName(customer.getName()));
-            findAndThen(contractMap, item.getContractId(), contract -> item.setContractNo(contract.getNo()));
-            findAndThen(receivableMap, item.getReceivableId(), receivable -> item.setReturnType(receivable.getReturnType()));
+        voPageResult.getList().forEach(receivablePlan -> {
+            findAndThen(userMap, receivablePlan.getOwnerUserId(), user -> receivablePlan.setOwnerUserName(user.getNickname()));
+            findAndThen(userMap, Long.parseLong(receivablePlan.getCreator()), user -> receivablePlan.setCreatorName(user.getNickname()));
+            findAndThen(customerMap, receivablePlan.getCustomerId(), customer -> receivablePlan.setCustomerName(customer.getName()));
+            findAndThen(contractMap, receivablePlan.getContractId(), contract -> receivablePlan.setContractNo(contract.getNo()));
+            findAndThen(receivableMap, receivablePlan.getReceivableId(), receivable -> receivablePlan.setReturnType(receivable.getReturnType()));
         });
-        return result;
-    }
-
-    static void setUserInfo(CrmReceivablePlanRespVO receivablePlan, Map<Long, AdminUserRespDTO> userMap) {
-        findAndThen(userMap, receivablePlan.getOwnerUserId(), user -> receivablePlan.setOwnerUserName(user.getNickname()));
-        findAndThen(userMap, Long.parseLong(receivablePlan.getCreator()), user -> receivablePlan.setCreatorName(user.getNickname()));
+        return voPageResult;
     }
 
     @Mapping(target = "bizId", source = "reqVO.id")

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

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

+ 5 - 7
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contactbusinesslink/CrmContactBusinessLinkDO.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/CrmContactBusinessDO.java

@@ -1,28 +1,26 @@
-package cn.iocoder.yudao.module.crm.dal.dataobject.contactbusinesslink;
+package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 
-// TODO @zyna:可以放到 contact 包下
 /**
- * CRM 联系人商机关联 DO
+ * CRM 联系人商机关联 DO
  *
  * @author 芋道源码
  */
-@TableName("crm_contact_business_link")
-@KeySequence("crm_contact_business_link_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@TableName("crm_contact_business")
+@KeySequence("crm_contact_business_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class CrmContactBusinessLinkDO extends BaseDO {
+public class CrmContactBusinessDO extends BaseDO {
 
     /**
      * 主键

+ 19 - 14
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contact/CrmContactDO.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.contact;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -29,9 +30,11 @@ public class CrmContactDO extends BaseDO {
     @TableId
     private Long id;
     /**
-     * 下次联系时间
+     * 客户编号
+     *
+     * 关联 {@link CrmCustomerDO#getId()}
      */
-    private LocalDateTime nextTime;
+    private Long customerId;
     /**
      * 手机号
      */
@@ -45,21 +48,20 @@ public class CrmContactDO extends BaseDO {
      */
     private String email;
     /**
-     * 客户编号
+     * 所在地
+     *
+     * 关联 {@link cn.iocoder.yudao.framework.ip.core.Area#getId()} 字段
      */
-    private Long customerId;
+    private Integer areaId;
     /**
-     * 地址
+     * 详细地址
      */
-    private String address;
+    private String detailAddress;
     /**
      * 备注
      */
     private String remark;
-    /**
-     * 最后跟进时间
-     */
-    private LocalDateTime lastTime;
+
     /**
      * 直属上级
      *
@@ -100,10 +102,13 @@ public class CrmContactDO extends BaseDO {
     private Long ownerUserId;
 
     /**
-     * 所在地
-     *
-     * 关联 {@link cn.iocoder.yudao.framework.ip.core.Area#getId()} 字段
+     * 最后跟进时间
      */
-    private Integer areaId;
+    private LocalDateTime contactLastTime;
+    // TODO @puhui999:增加一个字段 contactLastContent;最后跟进内容
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime contactNextTime;
 
 }

+ 6 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/customer/CrmCustomerDO.java

@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
 // TODO 芋艿:调整下字段
 
 /**
- * 客户 DO
+ * CRM 客户 DO
  *
  * @author Wanwan
  */
@@ -104,17 +104,21 @@ public class CrmCustomerDO extends BaseDO {
      */
     private Long ownerUserId;
     /**
-     * 地区编号
+     * 所在地
+     *
+     * 关联 {@link cn.iocoder.yudao.framework.ip.core.Area#getId()} 字段
      */
     private Integer areaId;
     /**
      * 详细地址
      */
     private String detailAddress;
+
     /**
      * 最后跟进时间
      */
     private LocalDateTime contactLastTime;
+    // TODO @puhui999:增加一个字段 contactLastContent;最后跟进内容
     /**
      * 下次联系时间
      */

+ 81 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/followup/CrmFollowUpRecordDO.java

@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.crm.dal.dataobject.followup;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
+import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+// TODO @puhui999:界面:做成一个 list 列表,字段是 id、跟进人、跟进方式、跟进时间、跟进内容、下次联系时间、关联联系人、关联商机
+// TODO @puhui999:界面:记录时,弹窗,表单字段是跟进方式、跟进内容、下次联系时间、关联联系人、关联商机;其中关联联系人、关联商机,要做成对应的组件列。
+/**
+ * 跟进记录 DO
+ *
+ * 用于记录客户、联系人的每一次跟进
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "crm_follow_up_record")
+@KeySequence("crm_follow_up_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CrmFollowUpRecordDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 数据类型
+     *
+     * 枚举 {@link CrmBizTypeEnum}
+     */
+    private Integer bizType;
+    /**
+     * 数据编号
+     *
+     * 关联 {@link CrmBizTypeEnum} 对应模块 DO 的 id 字段
+     */
+    private Long bizId;
+
+    /**
+     * 跟进类型
+     *
+     * TODO @puhui999:可以搞个数据字典,打电话、发短信、上门拜访、微信、邮箱、QQ
+     */
+    private Integer type;
+    /**
+     * 跟进内容
+     */
+    private String content;
+    /**
+     * 下次联系时间
+     */
+    private LocalDateTime nextTime;
+
+    /**
+     * 关联的商机编号数组
+     *
+     * 关联 {@link CrmBusinessDO#getId()}
+     */
+    private List<Long> businessIds;
+    /**
+     * 关联的联系人编号数组
+     *
+     * 关联 {@link CrmContactDO#getId()}
+     */
+    private List<Long> contactIds;
+
+}

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

@@ -30,7 +30,14 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
 
     default PageResult<CrmBusinessDO> selectPageByCustomerId(CrmBusinessPageReqVO pageReqVO) {
         return selectPage(pageReqVO, new LambdaQueryWrapperX<CrmBusinessDO>()
-                .eq(CrmBusinessDO::getCustomerId, pageReqVO.getCustomerId())  // 指定客户编号
+                .eq(CrmBusinessDO::getCustomerId, pageReqVO.getCustomerId()) // 指定客户编号
+                .likeIfPresent(CrmBusinessDO::getName, pageReqVO.getName())
+                .orderByDesc(CrmBusinessDO::getId));
+    }
+
+    default PageResult<CrmBusinessDO> selectPageByContactId(CrmBusinessPageReqVO pageReqVO, Collection<Long> businessIds) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<CrmBusinessDO>()
+                .in(CrmBusinessDO::getId, businessIds) // 指定商机编号
                 .likeIfPresent(CrmBusinessDO::getName, pageReqVO.getName())
                 .orderByDesc(CrmBusinessDO::getId));
     }
@@ -38,11 +45,8 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
     default PageResult<CrmBusinessDO> selectPage(CrmBusinessPageReqVO pageReqVO, Long userId) {
         MPJLambdaWrapperX<CrmBusinessDO> query = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        boolean condition = CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_BUSINESS.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_BUSINESS.getType(),
                 CrmBusinessDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE);
-        if (!condition) {
-            return PageResult.empty();
-        }
         // 拼接自身的查询条件
         query.selectAll(CrmBusinessDO.class)
                 .likeIfPresent(CrmBusinessDO::getName, pageReqVO.getName())

+ 1 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/clue/CrmClueMapper.java

@@ -30,11 +30,8 @@ public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
     default PageResult<CrmClueDO> selectPage(CrmCluePageReqVO pageReqVO, Long userId) {
         MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        boolean condition = CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_LEADS.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_LEADS.getType(),
                 CrmClueDO::getId, userId, pageReqVO.getSceneType(), pageReqVO.getPool());
-        if (!condition) {
-            return PageResult.empty();
-        }
         // 拼接自身的查询条件
         query.selectAll(CrmClueDO.class)
                 .likeIfPresent(CrmClueDO::getName, pageReqVO.getName())

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

@@ -43,11 +43,8 @@ public interface CrmContactMapper extends BaseMapperX<CrmContactDO> {
     default PageResult<CrmContactDO> selectPage(CrmContactPageReqVO pageReqVO, Long userId) {
         MPJLambdaWrapperX<CrmContactDO> query = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        boolean condition = CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTACT.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTACT.getType(),
                 CrmContactDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE);
-        if (!condition) {
-            return PageResult.empty();
-        }
         // 拼接自身的查询条件
         query.selectAll(CrmContactDO.class)
                 .likeIfPresent(CrmContactDO::getName, pageReqVO.getName())

+ 0 - 30
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contactbusinesslink/CrmContactBusinessLinkMapper.java

@@ -1,30 +0,0 @@
-package cn.iocoder.yudao.module.crm.dal.mysql.contactbusinesslink;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo.CrmContactBusinessLinkPageReqVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contactbusinesslink.CrmContactBusinessLinkDO;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * CRM 联系人商机关联 Mapper
- *
- * @author 芋道源码
- */
-@Mapper
-public interface CrmContactBusinessLinkMapper extends BaseMapperX<CrmContactBusinessLinkDO> {
-
-    default PageResult<CrmContactBusinessLinkDO> selectPage(CrmContactBusinessLinkPageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<CrmContactBusinessLinkDO>()
-                .eqIfPresent(CrmContactBusinessLinkDO::getContactId, reqVO.getContactId())
-                .eqIfPresent(CrmContactBusinessLinkDO::getBusinessId, reqVO.getBusinessId())
-                .betweenIfPresent(CrmContactBusinessLinkDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(CrmContactBusinessLinkDO::getId));
-    } // TODO @zyna:方法和方法之间要有空行
-    default PageResult<CrmContactBusinessLinkDO> selectPageByContact(CrmContactBusinessLinkPageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<CrmContactBusinessLinkDO>()
-                .eqIfPresent(CrmContactBusinessLinkDO::getContactId, reqVO.getContactId())
-                .orderByDesc(CrmContactBusinessLinkDO::getId));
-    }
-}

+ 34 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contactbusinesslink/CrmContactBusinessMapper.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.contactbusinesslink;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactBusinessDO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * CRM 联系人与商机的关联 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface CrmContactBusinessMapper extends BaseMapperX<CrmContactBusinessDO> {
+
+    default CrmContactBusinessDO selectByContactIdAndBusinessId(Long contactId, Long businessId) {
+        return selectOne(CrmContactBusinessDO::getContactId, contactId,
+                CrmContactBusinessDO::getBusinessId, businessId);
+    }
+
+    default void deleteByContactIdAndBusinessId(Long contactId, Collection<Long> businessIds) {
+        delete(new LambdaQueryWrapper<CrmContactBusinessDO>()
+                .eq(CrmContactBusinessDO::getContactId, contactId)
+                .in(CrmContactBusinessDO::getBusinessId, businessIds));
+    }
+
+    default List<CrmContactBusinessDO> selectListByContactId(Long contactId) {
+        return selectList(CrmContactBusinessDO::getContactId, contactId);
+    }
+
+}

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

@@ -41,11 +41,8 @@ public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
     default PageResult<CrmContractDO> selectPage(CrmContractPageReqVO pageReqVO, Long userId) {
         MPJLambdaWrapperX<CrmContractDO> mpjLambdaWrapperX = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        boolean condition = CrmQueryWrapperUtils.appendPermissionCondition(mpjLambdaWrapperX, CrmBizTypeEnum.CRM_CONTACT.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(mpjLambdaWrapperX, CrmBizTypeEnum.CRM_CONTACT.getType(),
                 CrmContractDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE);
-        if (!condition) {
-            return PageResult.empty();
-        }
         // 拼接自身的查询条件
         mpjLambdaWrapperX.selectAll(CrmContractDO.class)
                 .likeIfPresent(CrmContractDO::getNo, pageReqVO.getNo())

+ 13 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerLimitConfigMapper.java

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmC
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.List;
+
 /**
  * 客户限制配置 Mapper
  *
@@ -21,4 +23,15 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
                 .orderByDesc(CrmCustomerLimitConfigDO::getId));
     }
 
+    default List<CrmCustomerLimitConfigDO> selectListByTypeAndUserIdAndDeptId(
+            Integer type, Long userId, Long deptId) {
+        LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
+                .eq(CrmCustomerLimitConfigDO::getType, type);
+        query.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
+        if (deptId != null) {
+            query.apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
+        }
+        return selectList(query);
+    }
+
 }

+ 15 - 4
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.dal.mysql.customer;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.CrmCustomerPageReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
@@ -9,6 +10,7 @@ import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.util.CrmQueryWrapperUtils;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.springframework.lang.Nullable;
 
 import java.util.Collection;
 import java.util.List;
@@ -21,6 +23,18 @@ import java.util.List;
 @Mapper
 public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
 
+    default Long selectCountByLockStatusAndOwnerUserId(Boolean lockStatus, Long ownerUserId) {
+        return selectCount(new LambdaUpdateWrapper<CrmCustomerDO>()
+                .eq(CrmCustomerDO::getLockStatus, lockStatus)
+                .eq(CrmCustomerDO::getOwnerUserId, ownerUserId));
+    }
+
+    default Long selectCountByDealStatusAndOwnerUserId(@Nullable Boolean dealStatus, Long ownerUserId) {
+        return selectCount(new LambdaQueryWrapperX<CrmCustomerDO>()
+                .eqIfPresent(CrmCustomerDO::getDealStatus, dealStatus)
+                .eq(CrmCustomerDO::getOwnerUserId, ownerUserId));
+    }
+
     default int updateOwnerUserIdById(Long id, Long ownerUserId) {
         return update(new LambdaUpdateWrapper<CrmCustomerDO>()
                 .eq(CrmCustomerDO::getId, id)
@@ -30,11 +44,8 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
     default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO pageReqVO, Long userId) {
         MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        boolean condition = CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
                 CrmCustomerDO::getId, userId, pageReqVO.getSceneType(), pageReqVO.getPool());
-        if (!condition) {
-            return PageResult.empty();
-        }
         // 拼接自身的查询条件
         query.selectAll(CrmCustomerDO.class)
                 .likeIfPresent(CrmCustomerDO::getName, pageReqVO.getName())

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

@@ -39,11 +39,8 @@ public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
     default PageResult<CrmReceivableDO> selectPage(CrmReceivablePageReqVO pageReqVO, Long userId) {
         MPJLambdaWrapperX<CrmReceivableDO> query = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        boolean condition = CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE.getType(),
                 CrmReceivableDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE);
-        if (!condition) {
-            return PageResult.empty();
-        }
         // 拼接自身的查询条件
         query.selectAll(CrmReceivableDO.class)
                 .eqIfPresent(CrmReceivableDO::getNo, pageReqVO.getNo())

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

@@ -38,11 +38,8 @@ public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO
     default PageResult<CrmReceivablePlanDO> selectPage(CrmReceivablePlanPageReqVO pageReqVO, Long userId) {
         MPJLambdaWrapperX<CrmReceivablePlanDO> query = new MPJLambdaWrapperX<>();
         // 拼接数据权限的查询条件
-        boolean condition = CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(),
+        CrmQueryWrapperUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(),
                 CrmReceivablePlanDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE);
-        if (!condition) {
-            return PageResult.empty();
-        }
         // 拼接自身的查询条件
         query.selectAll(CrmReceivablePlanDO.class)
                 .eqIfPresent(CrmReceivablePlanDO::getCustomerId, pageReqVO.getCustomerId())

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

@@ -1 +0,0 @@
-package cn.iocoder.yudao.module.crm.framework.bizlog;

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

@@ -1 +0,0 @@
-package cn.iocoder.yudao.module.crm.framework.core;

+ 5 - 14
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/function/CrmIndustryParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmIndustryParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.bizlog.function;
+package cn.iocoder.yudao.module.crm.framework.operatelog.core;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
@@ -8,9 +8,8 @@ import org.springframework.stereotype.Component;
 
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_INDUSTRY;
 
-// TODO @puhui999:包名使用 operatelog 更合适哈;
 /**
- * 自定义函数-通过行业编号获取行业信息
+ * 行业的 {@link IParseFunction} 实现类
  *
  * @author HUIHUI
  */
@@ -30,18 +29,10 @@ public class CrmIndustryParseFunction implements IParseFunction {
 
     @Override
     public String apply(Object value) {
-        if (value == null) {
+        if (StrUtil.isEmptyIfStr(value)) {
             return "";
         }
-        if (StrUtil.isEmpty(value.toString())) {
-            return "";
-        }
-
-        // 获取行业信息
-        try {
-            return DictFrameworkUtils.getDictDataLabel(CRM_CUSTOMER_INDUSTRY, value.toString());
-        } catch (Exception ignored) {
-        }
-        return "";
+        return DictFrameworkUtils.getDictDataLabel(CRM_CUSTOMER_INDUSTRY, value.toString());
     }
+
 }

+ 5 - 13
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/function/CrmLevelParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmLevelParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.bizlog.function;
+package cn.iocoder.yudao.module.crm.framework.operatelog.core;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_LEVEL;
 
 /**
- * 自定义函数-通过客户等级编号获取客户等级信息
+ * 客户等级的 {@link IParseFunction} 实现类
  *
  * @author HUIHUI
  */
@@ -29,18 +29,10 @@ public class CrmLevelParseFunction implements IParseFunction {
 
     @Override
     public String apply(Object value) {
-        if (value == null) {
+        if (StrUtil.isEmptyIfStr(value)) {
             return "";
         }
-        if (StrUtil.isEmpty(value.toString())) {
-            return "";
-        }
-
-        // 获取客户等级信息
-        try {
-            return DictFrameworkUtils.getDictDataLabel(CRM_CUSTOMER_LEVEL, value.toString());
-        } catch (Exception ignored) {
-        }
-        return "";
+        return DictFrameworkUtils.getDictDataLabel(CRM_CUSTOMER_LEVEL, value.toString());
     }
+
 }

+ 5 - 13
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/bizlog/function/CrmSourceParseFunction.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/operatelog/core/CrmSourceParseFunction.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.bizlog.function;
+package cn.iocoder.yudao.module.crm.framework.operatelog.core;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_CUSTOMER_SOURCE;
 
 /**
- * 自定义函数-通过客户来源编号获取客户来源信息
+ * 客户来源的 {@link IParseFunction} 实现类
  *
  * @author HUIHUI
  */
@@ -29,18 +29,10 @@ public class CrmSourceParseFunction implements IParseFunction {
 
     @Override
     public String apply(Object value) {
-        if (value == null) {
+        if (StrUtil.isEmptyIfStr(value)) {
             return "";
         }
-        if (StrUtil.isEmpty(value.toString())) {
-            return "";
-        }
-
-        // 获取客户来源信息
-        try {
-            return DictFrameworkUtils.getDictDataLabel(CRM_CUSTOMER_SOURCE, value.toString());
-        } catch (Exception ignored) {
-        }
-        return "";
+        return DictFrameworkUtils.getDictDataLabel(CRM_CUSTOMER_SOURCE, value.toString());
     }
+
 }

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

@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.crm.framework.operatelog;

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/annotations/CrmPermission.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/annotations/CrmPermission.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.core.annotations;
+package cn.iocoder.yudao.module.crm.framework.permission.core.annotations;
 
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;

+ 25 - 17
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/core/aop/CrmPermissionAspect.java → yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/aop/CrmPermissionAspect.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.crm.framework.core.aop;
+package cn.iocoder.yudao.module.crm.framework.permission.core.aop;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
@@ -6,27 +6,27 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
-import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
+import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.permission.core.util.CrmPermissionUtils;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Before;
 import org.springframework.stereotype.Component;
 
-import jakarta.annotation.Resource;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.anyMatch;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_DENIED;
-import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_MODEL_NOT_EXISTS;
 
-// TODO 这个包,改成 permission,然后搞 config 和 core 包,这个类在 core 包里;目的是:framework 最好分类下
 /**
  * Crm 数据权限校验 AOP 切面
  *
@@ -42,10 +42,6 @@ public class CrmPermissionAspect {
 
     @Before("@annotation(crmPermission)")
     public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
-        // TODO 芋艿:临时,方便大家调试
-        //if (true) {
-        //    return;
-        //}
         // 获取相关属性值
         Map<String, Object> expressionValues = parseExpressions(joinPoint, crmPermission);
         Integer bizType = StrUtil.isEmpty(crmPermission.bizTypeValue()) ?
@@ -53,16 +49,28 @@ public class CrmPermissionAspect {
         Long bizId = (Long) expressionValues.get(crmPermission.bizId()); // 模块数据编号
         Integer permissionLevel = crmPermission.level().getLevel(); // 需要的权限级别
 
-        // TODO 如果是超级管理员则直接通过
-        //if (superAdmin){
-        //    return;
-        //}
-
-        // 1. 获取数据权限
+        // 1.1 如果是超级管理员则直接通过
+        if (CrmPermissionUtils.isCrmAdmin()) {
+            return;
+        }
+        // 1.2 获取数据权限
         List<CrmPermissionDO> bizPermissions = crmPermissionService.getPermissionListByBiz(bizType, bizId);
-        if (CollUtil.isEmpty(bizPermissions)) { // 数据权限不存存那么数据也不存在
-            throw exception(CRM_PERMISSION_MODEL_NOT_EXISTS, CrmBizTypeEnum.getNameByType(bizType));
+        if (CollUtil.isEmpty(bizPermissions)) { // 没有数据权限的情况
+            // 公海数据如果没有团队成员大家也因该有读权限才对
+            if (CrmPermissionLevelEnum.isRead(permissionLevel)) {
+                return;
+            }
+
+            // 没有数据权限的情况下超出了读权限直接报错,避免后面校验空指针
+            throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType));
+        } else { // 有数据权限但是没有负责人的情况
+            if (!anyMatch(bizPermissions, item -> CrmPermissionLevelEnum.isOwner(item.getLevel()))) {
+                if (CrmPermissionLevelEnum.isRead(permissionLevel)) {
+                    return;
+                }
+            }
         }
+
         // 2.1 情况一:如果自己是负责人,则默认有所有权限
         CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, permission -> ObjUtil.equal(permission.getUserId(), getUserId()));
         if (userPermission != null) {

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

@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.crm.framework.permission.core;

+ 40 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/util/CrmPermissionUtils.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.crm.framework.permission.core.util;
+
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionRoleCodeEnum;
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
+
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+/**
+ * 数据权限工具类
+ *
+ * @author HUIHUI
+ */
+public class CrmPermissionUtils {
+
+    /**
+     * 校验用户是否是 CRM 管理员
+     *
+     * @return 是/否
+     */
+    public static boolean isCrmAdmin() {
+        return SingletonManager.getPermissionApi().hasAnyRoles(getLoginUserId(), CrmPermissionRoleCodeEnum.CRM_ADMIN.getCode());
+    }
+
+    /**
+     * 静态内部类实现单例获取
+     *
+     * @author HUIHUI
+     */
+    private static class SingletonManager {
+
+        private static final PermissionApi PERMISSION_API = SpringUtil.getBean(PermissionApi.class);
+
+        public static PermissionApi getPermissionApi() {
+            return PERMISSION_API;
+        }
+
+    }
+
+}

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

@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.crm.framework.permission;

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

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusi
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
 import jakarta.validation.Valid;
 
@@ -75,10 +76,20 @@ public interface CrmBusinessService {
      * 数据权限:基于 {@link CrmCustomerDO} 读取
      *
      * @param pageReqVO 分页查询
-     * @return 联系人分页
+     * @return 商机分页
      */
     PageResult<CrmBusinessDO> getBusinessPageByCustomerId(CrmBusinessPageReqVO pageReqVO);
 
+    /**
+     * 获得商机分页,基于指定联系人
+     *
+     * 数据权限:基于 {@link CrmContactDO} 读取
+     *
+     * @param pageReqVO 分页参数
+     * @return 商机分页
+     */
+    PageResult<CrmBusinessDO> getBusinessPageByContact(CrmBusinessPageReqVO pageReqVO);
+
     /**
      * 商机转移
      *

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

@@ -9,11 +9,12 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusi
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateReqVO;
 import cn.iocoder.yudao.module.crm.convert.business.CrmBusinessConvert;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
-import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
-import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.service.contact.CrmContactBusinessService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
 import jakarta.annotation.Resource;
@@ -25,6 +26,7 @@ import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
 
 /**
@@ -38,24 +40,26 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
 
     @Resource
     private CrmBusinessMapper businessMapper;
-    @Resource
-    private CrmCustomerService customerService;
 
     @Resource
-    private CrmPermissionService crmPermissionService;
+    private CrmPermissionService permissionService;
+    @Resource
+    private CrmContactBusinessService contactBusinessService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    // TODO @商机待定:操作日志;
     public Long createBusiness(CrmBusinessCreateReqVO createReqVO, Long userId) {
-        // 插入
+        // 1. 插入商机
         CrmBusinessDO business = CrmBusinessConvert.INSTANCE.convert(createReqVO);
         businessMapper.insert(business);
+        // TODO 商机待定:插入商机与产品的关联表;校验商品存在
 
-        // 创建数据权限
-        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType())
-                .setBizId(business.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
+        // TODO 商机待定:在联系人的详情页,如果直接【新建商机】,则需要关联下。这里要搞个 CrmContactBusinessDO 表
 
-        // 返回
+        // 2. 创建数据权限
+        permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType())
+                .setBizId(business.getId()).setUserId(userId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
         return business.getId();
     }
 
@@ -63,12 +67,17 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     @Transactional(rollbackFor = Exception.class)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_BUSINESS, bizId = "#updateReqVO.id",
             level = CrmPermissionLevelEnum.WRITE)
+    // TODO @商机待定:操作日志;
     public void updateBusiness(CrmBusinessUpdateReqVO updateReqVO) {
-        // 校验存在
+        // 1. 校验存在
         validateBusinessExists(updateReqVO.getId());
-        // 更新
+
+        // 2. 更新商机
         CrmBusinessDO updateObj = CrmBusinessConvert.INSTANCE.convert(updateReqVO);
         businessMapper.updateById(updateObj);
+        // TODO 商机待定:插入商机与产品的关联表;校验商品存在
+
+        // TODO @商机待定:如果状态发生变化,插入商机状态变更记录表
     }
 
     @Override
@@ -77,10 +86,12 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
     public void deleteBusiness(Long id) {
         // 校验存在
         validateBusinessExists(id);
+        // TODO @商机待定:需要校验有没关联合同。CrmContractDO 的 businessId 字段
+
         // 删除
         businessMapper.deleteById(id);
         // 删除数据权限
-        crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_BUSINESS.getType(), id);
+        permissionService.deletePermission(CrmBizTypeEnum.CRM_BUSINESS.getType(), id);
     }
 
     private CrmBusinessDO validateBusinessExists(Long id) {
@@ -116,19 +127,32 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         return businessMapper.selectPageByCustomerId(pageReqVO);
     }
 
+    @Override
+    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#pageReqVO.contactId", level = CrmPermissionLevelEnum.READ)
+    public PageResult<CrmBusinessDO> getBusinessPageByContact(CrmBusinessPageReqVO pageReqVO) {
+        // 1. 查询关联的商机编号
+        List<CrmContactBusinessDO> contactBusinessList = contactBusinessService.getContactBusinessListByContactId(
+                pageReqVO.getContactId());
+        if (CollUtil.isEmpty(contactBusinessList)) {
+            return PageResult.empty();
+        }
+        // 2. 查询商机分页
+        return businessMapper.selectPageByContactId(pageReqVO,
+                convertSet(contactBusinessList, CrmContactBusinessDO::getBusinessId));
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
+    // TODO @puhui999:操作日志
     public void transferBusiness(CrmBusinessTransferReqVO reqVO, Long userId) {
         // 1 校验商机是否存在
         validateBusinessExists(reqVO.getId());
 
         // 2.1 数据权限转移
-        crmPermissionService.transferPermission(
+        permissionService.transferPermission(
                 CrmBusinessConvert.INSTANCE.convert(reqVO, userId).setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()));
         // 2.2 设置新的负责人
         businessMapper.updateOwnerUserIdById(reqVO.getId(), reqVO.getNewOwnerUserId());
-
-        // 3. TODO 记录转移日志
     }
 
 }

+ 10 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessStatusService.java

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.status.CrmBusine
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
 
 import jakarta.validation.Valid;
+
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -64,4 +66,12 @@ public interface CrmBusinessStatusService {
      */
     List<CrmBusinessStatusDO> selectList(CrmBusinessStatusQueryVO queryVO);
 
+    /**
+     * 获得商机状态列表
+     *
+     * @param ids 编号数组
+     * @return 商机状态列表
+     */
+    List<CrmBusinessStatusDO> getBusinessStatusList(Collection<Long> ids);
+
 }

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

@@ -12,6 +12,7 @@ import org.springframework.validation.annotation.Validated;
 
 import jakarta.annotation.Resource;
 
+import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -77,4 +78,9 @@ public class CrmBusinessStatusServiceImpl implements CrmBusinessStatusService {
         return businessStatusMapper.selectList(queryVO);
     }
 
+    @Override
+    public List<CrmBusinessStatusDO> getBusinessStatusList(Collection<Long> ids) {
+        return businessStatusMapper.selectBatchIds(ids);
+    }
+
 }

+ 10 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessStatusTypeService.java

@@ -5,8 +5,9 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.type.CrmBusiness
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.type.CrmBusinessStatusTypeQueryVO;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.type.CrmBusinessStatusTypeSaveReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusTypeDO;
-
 import jakarta.validation.Valid;
+
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -63,4 +64,12 @@ public interface CrmBusinessStatusTypeService {
      */
     List<CrmBusinessStatusTypeDO> selectList(CrmBusinessStatusTypeQueryVO queryVO);
 
+    /**
+     * 获得商机状态类型列表
+     *
+     * @param ids 编号数组
+     * @return 商机状态类型列表
+     */
+    List<CrmBusinessStatusTypeDO> getBusinessStatusTypeList(Collection<Long> ids);
+
 }

+ 7 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessStatusTypeServiceImpl.java

@@ -17,6 +17,8 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import jakarta.annotation.Resource;
+
+import java.util.Collection;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -118,4 +120,9 @@ public class CrmBusinessStatusTypeServiceImpl implements CrmBusinessStatusTypeSe
         return businessStatusTypeMapper.selectList(queryVO);
     }
 
+    @Override
+    public List<CrmBusinessStatusTypeDO> getBusinessStatusTypeList(Collection<Long> ids) {
+        return businessStatusTypeMapper.selectBatchIds(ids);
+    }
+
 }

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

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

+ 1 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java

@@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
-import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import jakarta.annotation.Resource;

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

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

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

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.crm.service.contact;
+
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactBusinessReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactBusinessDO;
+import jakarta.validation.Valid;
+
+import java.util.List;
+
+/**
+ * CRM 联系人与商机的关联 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface CrmContactBusinessService {
+
+    /**
+     * 创建联系人与商机的关联
+     *
+     * @param createReqVO 创建信息
+     */
+    void createContactBusinessList(@Valid CrmContactBusinessReqVO createReqVO);
+
+    /**
+     * 删除联系人与商机的关联
+     *
+     * @param deleteReqVO 删除信息
+     */
+    void deleteContactBusinessList(@Valid CrmContactBusinessReqVO deleteReqVO);
+
+    /**
+     * 获得联系人与商机的关联列表,基于联系人编号
+     *
+     * @param contactId 联系人编号
+     * @return 联系人商机关联
+     */
+    List<CrmContactBusinessDO> getContactBusinessListByContactId(Long contactId);
+
+}

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

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.crm.service.contact;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactBusinessReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.contactbusinesslink.CrmContactBusinessMapper;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BUSINESS_NOT_EXISTS;
+import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTACT_NOT_EXISTS;
+
+// TODO @puhui999:数据权限的校验;每个操作;
+/**
+ * 联系人与商机的关联 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class CrmContactBusinessServiceImpl implements CrmContactBusinessService {
+
+    @Resource
+    private CrmContactBusinessMapper contactBusinessMapper;
+
+    @Resource
+    @Lazy // 延迟加载,为了解决延迟加载
+    private CrmBusinessService businessService;
+    @Resource
+    @Lazy // 延迟加载,为了解决延迟加载
+    private CrmContactService contactService;
+
+    @Override
+    public void createContactBusinessList(CrmContactBusinessReqVO createReqVO) {
+        CrmContactDO contact = contactService.getContact(createReqVO.getContactId());
+        if (contact == null) {
+            throw exception(CONTACT_NOT_EXISTS);
+        }
+        // 遍历处理,考虑到一般数量不会太多,代码处理简单
+        List<CrmContactBusinessDO> saveDOList = new ArrayList<>();
+        createReqVO.getBusinessIds().forEach(businessId -> {
+            CrmBusinessDO business = businessService.getBusiness(businessId);
+            if (business == null) {
+                throw exception(BUSINESS_NOT_EXISTS);
+            }
+            // 关联判重
+            if (contactBusinessMapper.selectByContactIdAndBusinessId(createReqVO.getContactId(), businessId) != null) {
+                return;
+            }
+            saveDOList.add(new CrmContactBusinessDO(null, createReqVO.getContactId(), businessId));
+        });
+        // 批量插入
+        if (CollUtil.isNotEmpty(saveDOList)) {
+            contactBusinessMapper.insertBatch(saveDOList);
+        }
+    }
+
+    @Override
+    public void deleteContactBusinessList(CrmContactBusinessReqVO deleteReqVO) {
+        CrmContactDO contact = contactService.getContact(deleteReqVO.getContactId());
+        if (contact == null) {
+            throw exception(CONTACT_NOT_EXISTS);
+        }
+        // 直接删除
+        contactBusinessMapper.deleteByContactIdAndBusinessId(
+                deleteReqVO.getContactId(), deleteReqVO.getBusinessIds());
+    }
+
+    @Override
+    public List<CrmContactBusinessDO> getContactBusinessListByContactId(Long contactId) {
+        return contactBusinessMapper.selectListByContactId(contactId);
+    }
+
+}

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

@@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
 import cn.iocoder.yudao.module.crm.dal.mysql.contact.CrmContactMapper;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
-import cn.iocoder.yudao.module.crm.framework.core.annotations.CrmPermission;
+import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
 import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
 import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
 import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
@@ -43,33 +43,38 @@ public class CrmContactServiceImpl implements CrmContactService {
     private CrmCustomerService customerService;
     @Resource
     private CrmPermissionService crmPermissionService;
-
     @Resource
     private AdminUserApi adminUserApi;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    // TODO @zyna:增加操作日志,可以参考 CustomerService;内容是 新建了联系人【名字】
     public Long createContact(CrmContactCreateReqVO createReqVO, Long userId) {
-        // 1.1 校验
+        // 1. 校验
         validateRelationDataExists(createReqVO);
-        // 1.2 插入
+
+        // 2. 插入联系人
         CrmContactDO contact = CrmContactConvert.INSTANCE.convert(createReqVO);
         contactMapper.insert(contact);
 
-        // 2. 创建数据权限
+        // 3. 创建数据权限
         crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(userId)
                 .setBizType(CrmBizTypeEnum.CRM_CONTACT.getType()).setBizId(contact.getId())
                 .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+
+        // TODO @zyna:特殊逻辑:如果在【商机】详情那,点击【新增联系人】时,可以自动绑定商机
         return contact.getId();
     }
 
     @Override
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
+    // TODO @zyna:增加操作日志,可以参考 CustomerService;需要 diff 出字段
     public void updateContact(CrmContactUpdateReqVO updateReqVO) {
         // 1. 校验存在
         validateContactExists(updateReqVO.getId());
         validateRelationDataExists(updateReqVO);
-        // 2. 更新
+
+        // 2. 更新联系人
         CrmContactDO updateObj = CrmContactConvert.INSTANCE.convert(updateReqVO);
         contactMapper.updateById(updateObj);
     }
@@ -99,10 +104,15 @@ public class CrmContactServiceImpl implements CrmContactService {
     public void deleteContact(Long id) {
         // 校验存在
         validateContactExists(id);
+        // TODO @zyna:如果有关联的合同,不允许删除;Contract.contactId
+
         // 删除
         contactMapper.deleteById(id);
         // 删除数据权限
         crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_CONTACT.getType(), id);
+        // TODO @zyna:删除商机联系人关联
+
+        // TODO @puhui999:删除跟进记录
     }
 
     private void validateContactExists(Long id) {
@@ -137,6 +147,8 @@ public class CrmContactServiceImpl implements CrmContactService {
     }
 
     @Override
+    // TODO @puhui999:权限校验
+    // TODO @puhui999:记录操作日志;将联系人【名字】转移给【新负责人】
     public void transferContact(CrmContactTransferReqVO reqVO, Long userId) {
         // 1 校验联系人是否存在
         validateContactExists(reqVO.getId());
@@ -150,4 +162,4 @@ public class CrmContactServiceImpl implements CrmContactService {
         // 3. TODO 记录转移日志
     }
 
-}
+}

+ 0 - 72
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contactbusinesslink/CrmContactBusinessLinkService.java

@@ -1,72 +0,0 @@
-package cn.iocoder.yudao.module.crm.service.contactbusinesslink;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo.CrmContactBusinessLinkPageReqVO;
-import cn.iocoder.yudao.module.crm.controller.admin.contactbusinesslink.vo.CrmContactBusinessLinkSaveReqVO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contactbusinesslink.CrmContactBusinessLinkDO;
-
-import jakarta.validation.Valid;
-import java.util.List;
-
-/**
- * CRM 联系人商机关联 Service 接口
- *
- * @author 芋道源码
- */
-public interface CrmContactBusinessLinkService {
-
-    /**
-     * 创建联系人商机关联
-     *
-     * @param createReqVO 创建信息
-     * @return 编号
-     */
-    Long createContactBusinessLink(@Valid CrmContactBusinessLinkSaveReqVO createReqVO);
-
-    /**
-     * 创建联系人商机关联
-     *
-     * @param createReqVO 创建信息
-     */
-    void createContactBusinessLinkBatch(@Valid List<CrmContactBusinessLinkSaveReqVO> createReqVO);
-
-    /**
-     * 更新联系人商机关联
-     *
-     * @param updateReqVO 更新信息
-     */
-    void updateContactBusinessLink(@Valid CrmContactBusinessLinkSaveReqVO updateReqVO);
-
-    /**
-     * 删除联系人商机关联
-     *
-     * @param createReqVO  删除列表
-     */
-    void deleteContactBusinessLink(@Valid List<CrmContactBusinessLinkSaveReqVO> createReqVO);
-
-    /**
-     * 获得联系人商机关联
-     *
-     * @param id 编号
-     * @return 联系人商机关联
-     */
-    CrmContactBusinessLinkDO getContactBusinessLink(Long id);
-
-    /**
-     * 获得联系人商机关联分页
-     *
-     * @param pageReqVO 编号
-     * @return 联系人商机关联
-     */
-    PageResult<CrmBusinessRespVO> getContactBusinessLinkPageByContact(CrmContactBusinessLinkPageReqVO pageReqVO);
-
-    /**
-     * 获得联系人商机关联分页
-     *
-     * @param pageReqVO 分页查询
-     * @return 联系人商机关联分页
-     */
-    PageResult<CrmContactBusinessLinkDO> getContactBusinessLinkPage(CrmContactBusinessLinkPageReqVO pageReqVO);
-
-}

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