Преглед изворни кода

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

YunaiV пре 10 месеци
родитељ
комит
ab00986a1d
50 измењених фајлова са 925 додато и 178 уклоњено
  1. 1 1
      yudao-dependencies/pom.xml
  2. 4 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  3. 9 10
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java
  4. 0 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
  5. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
  6. 10 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http
  7. 13 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.java
  8. 74 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsFunnelController.java
  9. 24 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByAreaRespVO.java
  10. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByProductRespVO.java
  11. 23 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticFunnelSummaryRespVO.java
  12. 21 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessInversionRateSummaryByDateRespVO.java
  13. 21 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByDateRespVO.java
  14. 25 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByEndStatusRespVO.java
  15. 47 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsFunnelReqVO.java
  16. 2 4
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticsPortraitReqVO.java
  17. 8 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
  18. 17 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java
  19. 30 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsFunnelMapper.java
  20. 9 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
  21. 6 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
  22. 1 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
  23. 6 6
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
  24. 1 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
  25. 16 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerService.java
  26. 44 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java
  27. 56 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelService.java
  28. 153 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelServiceImpl.java
  29. 82 9
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
  30. 8 6
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitServiceImpl.java
  31. 4 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java
  32. 49 7
      yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
  33. 87 0
      yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml
  34. 20 112
      yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
  35. 1 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
  36. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java
  37. 4 2
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
  38. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java
  39. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java
  40. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java
  41. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java
  42. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java
  43. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java
  44. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java
  45. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java
  46. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java
  47. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java
  48. 7 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java
  49. 10 0
      yudao-server/src/main/resources/application-dev.yaml
  50. 1 0
      yudao-server/src/main/resources/application-local.yaml

+ 1 - 1
yudao-dependencies/pom.xml

@@ -31,7 +31,7 @@
         <redisson.version>3.26.0</redisson.version>
         <dm8.jdbc.version>8.1.3.62</dm8.jdbc.version>
         <!-- 消息队列 -->
-        <rocketmq-spring.version>2.2.3</rocketmq-spring.version>
+        <rocketmq-spring.version>2.3.0</rocketmq-spring.version>
         <!-- 服务保障相关 -->
         <lock4j.version>2.2.7</lock4j.version>
         <!-- 监控相关 -->

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

@@ -97,6 +97,10 @@ public class CollectionUtils {
                 .collect(Collectors.toList());
     }
 
+    public static <T> Set<T> convertSet(Collection<T> from) {
+        return convertSet(from, v -> v);
+    }
+
     public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
         if (CollUtil.isEmpty(from)) {
             return new HashSet<>();

+ 9 - 10
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java

@@ -1,12 +1,11 @@
 package cn.iocoder.yudao.framework.mq.rabbitmq.config;
 
-import cn.hutool.core.util.ReflectUtil;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.amqp.utils.SerializationUtils;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.MessageConverter;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
-
-import java.lang.reflect.Field;
+import org.springframework.context.annotation.Bean;
 
 /**
  * RabbitMQ 消息队列配置类
@@ -18,12 +17,12 @@ import java.lang.reflect.Field;
 @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
 public class YudaoRabbitMQAutoConfiguration {
 
-    static {
-        // 强制设置 SerializationUtils 的 TRUST_ALL 为 true,避免 RabbitMQ Consumer 反序列化消息报错
-        // 为什么不通过设置 spring.amqp.deserialization.trust.all 呢?因为可能在 SerializationUtils static 初始化后
-        Field trustAllField = ReflectUtil.getField(SerializationUtils.class, "TRUST_ALL");
-        ReflectUtil.removeFinalModify(trustAllField);
-        ReflectUtil.setFieldValue(SerializationUtils.class, trustAllField, true);
+    /**
+     * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息
+     */
+    @Bean
+    public MessageConverter createMessageConverter() {
+        return new Jackson2JsonMessageConverter();
     }
 
 }

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java


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

@@ -177,7 +177,7 @@ public class CrmBusinessController {
                 buildBusinessDetailList(list));
     }
 
-    private List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
+    public List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
         if (CollUtil.isEmpty(list)) {
             return Collections.emptyList();
         }

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

@@ -53,3 +53,13 @@ tenant-id: {{adminTenentId}}
 GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-user?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
 Authorization: Bearer {{token}}
 tenant-id: {{adminTenentId}}
+
+### 6.3 获取客户成交周期(按区域)
+GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-area?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 6.4 获取客户成交周期(按产品)
+GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-product?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

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

@@ -96,6 +96,18 @@ public class CrmStatisticsCustomerController {
         return success(customerService.getCustomerDealCycleByUser(reqVO));
     }
 
-    // TODO dhb52:【成交周期分析】里,有按照员工(已实现)、地区(未实现)、产品(未实现),需要在看看哈;可以把 CustomerDealCycle 拆成 3 个 tab,员工客户成交周期分析、地区客户成交周期分析、产品客户成交周期分析;
+    @GetMapping("/get-customer-deal-cycle-by-area")
+    @Operation(summary = "获取客户成交周期(按用户)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsCustomerDealCycleByAreaRespVO>> getCustomerDealCycleByArea(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getCustomerDealCycleByArea(reqVO));
+    }
+
+    @GetMapping("/get-customer-deal-cycle-by-product")
+    @Operation(summary = "获取客户成交周期(按用户)")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+    public CommonResult<List<CrmStatisticsCustomerDealCycleByProductRespVO>> getCustomerDealCycleByProduct(@Valid CrmStatisticsCustomerReqVO reqVO) {
+        return success(customerService.getCustomerDealCycleByProduct(reqVO));
+    }
 
 }

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

@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics;
+
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.business.CrmBusinessController;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsFunnelService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - CRM 销售漏斗")
+@RestController
+@RequestMapping("/crm/statistics-funnel")
+@Validated
+public class CrmStatisticsFunnelController {
+
+    @Resource
+    private CrmStatisticsFunnelService funnelService;
+
+    @GetMapping("/get-funnel-summary")
+    @Operation(summary = "获取销售漏斗统计数据", description = "用于【销售漏斗】页面的【销售漏斗分析】")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+    public CommonResult<CrmStatisticFunnelSummaryRespVO> getFunnelSummary(@Valid CrmStatisticsFunnelReqVO reqVO) {
+        return success(funnelService.getFunnelSummary(reqVO));
+    }
+
+    @GetMapping("/get-business-summary-by-end-status")
+    @Operation(summary = "获取商机结束状态统计", description = "用于【销售漏斗】页面的【销售漏斗分析】")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+    public CommonResult<List<CrmStatisticsBusinessSummaryByEndStatusRespVO>> getBusinessSummaryByEndStatus(@Valid CrmStatisticsFunnelReqVO reqVO) {
+        return success(funnelService.getBusinessSummaryByEndStatus(reqVO));
+    }
+
+    @GetMapping("/get-business-summary-by-date")
+    @Operation(summary = "获取新增商机分析(按日期)", description = "用于【销售漏斗】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+    public CommonResult<List<CrmStatisticsBusinessSummaryByDateRespVO>> getBusinessSummaryByDate(@Valid CrmStatisticsFunnelReqVO reqVO) {
+        return success(funnelService.getBusinessSummaryByDate(reqVO));
+    }
+
+    @GetMapping("/get-business-inversion-rate-summary-by-date")
+    @Operation(summary = "获取商机转化率分析(按日期)", description = "用于【销售漏斗】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+    public CommonResult<List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO>> getBusinessInversionRateSummaryByDate(@Valid CrmStatisticsFunnelReqVO reqVO) {
+        return success(funnelService.getBusinessInversionRateSummaryByDate(reqVO));
+    }
+
+    @GetMapping("/get-business-page-by-date")
+    @Operation(summary = "获得商机分页(按日期)", description = "用于【销售漏斗】页面的【新增商机分析】")
+    @PreAuthorize("@ss.hasPermission('crm:business:query')")
+    public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPageByDate(@Valid CrmStatisticsFunnelReqVO pageVO) {
+        PageResult<CrmBusinessDO> pageResult = funnelService.getBusinessPageByDate(pageVO);
+        return success(new PageResult<>(buildBusinessDetailList(pageResult.getList()), pageResult.getTotal()));
+    }
+
+    private List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
+        return SpringUtil.getBean(CrmBusinessController.class).buildBusinessDetailList(list);
+    }
+
+}

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

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户成交周期分析(按区域) VO")
+@Data
+public class CrmStatisticsCustomerDealCycleByAreaRespVO {
+
+    @Schema(description = "省份编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @JsonIgnore
+    private Integer areaId;
+
+    @Schema(description = "省份名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "浙江省")
+    private String areaName;
+
+    @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
+    private Double customerDealCycle;
+
+    @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer customerDealCount;
+
+}

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

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

+ 23 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticFunnelSummaryRespVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Schema(description = "管理后台 - CRM 销售漏斗 Response VO")
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class CrmStatisticFunnelSummaryRespVO {
+
+    @Schema(description = "客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long customerCount;
+
+    @Schema(description = "商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long businessCount;
+
+    @Schema(description = "赢单数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long businessWinCount;
+
+}

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

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - CRM 商机转化率分析(按日期) VO")
+@Data
+public class CrmStatisticsBusinessInversionRateSummaryByDateRespVO {
+
+    @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+    private String time;
+
+    @Schema(description = "商机数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long businessCount;
+
+    @Schema(description = "赢单商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long businessWinCount;
+
+}

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

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - CRM 新增商机分析(按日期) VO")
+@Data
+public class CrmStatisticsBusinessSummaryByDateRespVO {
+
+    @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+    private String time;
+
+    @Schema(description = "新增商机数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long businessCreateCount;
+
+    @Schema(description = "新增商机金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private BigDecimal totalPrice;
+
+}

+ 25 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByEndStatusRespVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - CRM 商机结束状态统计 Response VO")
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class CrmStatisticsBusinessSummaryByEndStatusRespVO {
+
+    @Schema(description = "结束状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer endStatus;
+
+    @Schema(description = "商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long businessCount;
+
+    @Schema(description = "商机总金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private BigDecimal totalPrice;
+
+}

+ 47 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsFunnelReqVO.java

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

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

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
@@ -31,12 +32,9 @@ public class CrmStatisticsPortraitReqVO {
     @Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
     private List<Long> userIds;
 
-    /**
-     * 前端如果选择自定义时间, 那么前端传递起始-终止时间, 如果选择其他时间间隔类型, 则由后台计算起始-终止时间
-     * 并作为参数传递给Mapper
-     */
     @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @Size(min = 2, max = 2, message = "请选择时间范围")
     private LocalDateTime[] times;
 
 }

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

@@ -5,8 +5,8 @@ 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.business.vo.business.CrmBusinessPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -59,10 +59,16 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
         return selectCount(CrmBusinessDO::getStatusTypeId, statusTypeId);
     }
 
-    default List<CrmBusinessDO> selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId){
+    default List<CrmBusinessDO> selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
         return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
                 .eq(CrmBusinessDO::getCustomerId, customerId)
                 .eq(CrmBusinessDO::getOwnerUserId, ownerUserId));
     }
 
+    default PageResult<CrmBusinessDO> selectPage(CrmStatisticsFunnelReqVO pageVO) {
+        return selectPage(pageVO, new LambdaQueryWrapperX<CrmBusinessDO>()
+                .in(CrmBusinessDO::getOwnerUserId, pageVO.getUserIds())
+                .betweenIfPresent(CrmBusinessDO::getCreateTime, pageVO.getTimes()));
+    }
+
 }

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

@@ -53,6 +53,7 @@ public interface CrmStatisticsCustomerMapper {
 
     /**
      * 合同总金额(按用户)
+     *
      * @return 统计数据@return 统计数据@param reqVO 请求参数
      * @return 统计数据
      */
@@ -191,4 +192,20 @@ public interface CrmStatisticsCustomerMapper {
      */
     List<CrmStatisticsCustomerDealCycleByUserRespVO> selectCustomerDealCycleGroupByUser(CrmStatisticsCustomerReqVO reqVO);
 
+    /**
+     * 客户成交周期(按区域)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerDealCycleByAreaRespVO> selectCustomerDealCycleGroupByAreaId(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 客户成交周期(按产品)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerDealCycleByProductRespVO> selectCustomerDealCycleGroupByProductId(CrmStatisticsCustomerReqVO reqVO);
+
 }

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

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
+
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessInversionRateSummaryByDateRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByEndStatusRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * CRM 销售漏斗 Mapper
+ *
+ * @author HUIHUI
+ */
+@Mapper
+public interface CrmStatisticsFunnelMapper {
+
+    Long selectCustomerCountByDate(CrmStatisticsFunnelReqVO reqVO);
+
+    Long selectBusinessCountByDateAndEndStatus(@Param("reqVO") CrmStatisticsFunnelReqVO reqVO, @Param("status") Integer status);
+
+    List<CrmStatisticsBusinessSummaryByEndStatusRespVO> selectBusinessSummaryListGroupByEndStatus(CrmStatisticsFunnelReqVO reqVO);
+
+    List<CrmStatisticsBusinessSummaryByDateRespVO> selectBusinessSummaryGroupByDate(CrmStatisticsFunnelReqVO reqVO);
+
+    List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO> selectBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
+
+}

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

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusi
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessSaveReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateStatusReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
@@ -194,4 +195,12 @@ public interface CrmBusinessService {
      */
     List<CrmBusinessDO> getBusinessListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId);
 
+    /**
+     * 获得商机分页,目前用于【数据统计】
+     *
+     * @param pageVO 请求
+     * @return 商机分页
+     */
+    PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO);
+
 }

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

@@ -10,6 +10,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.CrmBusinessUpdateStatusReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactBusinessReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
@@ -375,4 +376,9 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         return businessMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
     }
 
+    @Override
+    public PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO) {
+        return businessMapper.selectPage(pageVO);
+    }
+
 }

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

@@ -120,7 +120,7 @@ public class CrmClueServiceImpl implements CrmClueService {
     }
 
     @Override
-    @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}",
+    @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}}",
             success = CRM_CLUE_FOLLOW_UP_SUCCESS)
     @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
     public void updateClueFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) {

+ 6 - 6
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java

@@ -39,7 +39,7 @@ public interface CrmCustomerService {
     /**
      * 更新客户的跟进状态
      *
-     * @param id        编号
+     * @param id         编号
      * @param dealStatus 跟进状态
      */
     void updateCustomerDealStatus(Long id, Boolean dealStatus);
@@ -47,8 +47,8 @@ public interface CrmCustomerService {
     /**
      * 更新客户相关的跟进信息
      *
-     * @param id 编号
-     * @param contactNextTime 下次联系时间
+     * @param id                 编号
+     * @param contactNextTime    下次联系时间
      * @param contactLastContent 最后联系内容
      */
     void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
@@ -99,8 +99,8 @@ public interface CrmCustomerService {
     /**
      * 获得放入公海提醒的客户分页
      *
-     * @param pageVO       分页查询
-     * @param userId       用户编号
+     * @param pageVO 分页查询
+     * @param userId 用户编号
      * @return 客户分页
      */
     PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId);
@@ -108,7 +108,7 @@ public interface CrmCustomerService {
     /**
      * 获得待进入公海的客户数量
      *
-     * @param userId       用户编号
+     * @param userId 用户编号
      * @return 提醒数量
      */
     Long getPutPoolRemindCustomerCount(Long userId);

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

@@ -47,6 +47,7 @@ import java.time.LocalDateTime;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;

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

@@ -93,4 +93,20 @@ public interface CrmStatisticsCustomerService {
      */
     List<CrmStatisticsCustomerDealCycleByUserRespVO> getCustomerDealCycleByUser(CrmStatisticsCustomerReqVO reqVO);
 
+    /**
+     * 客户成交周期(按区域)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerDealCycleByAreaRespVO> getCustomerDealCycleByArea(CrmStatisticsCustomerReqVO reqVO);
+
+    /**
+     * 客户成交周期(按产品)
+     *
+     * @param reqVO 请求参数
+     * @return 统计数据
+     */
+    List<CrmStatisticsCustomerDealCycleByProductRespVO> getCustomerDealCycleByProduct(CrmStatisticsCustomerReqVO reqVO);
+
 }

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

@@ -4,6 +4,9 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.framework.ip.core.Area;
+import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
 import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsCustomerMapper;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -19,6 +22,7 @@ import java.time.LocalDateTime;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import java.util.stream.Stream;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -290,6 +294,46 @@ public class CrmStatisticsCustomerServiceImpl implements CrmStatisticsCustomerSe
         return summaryList;
     }
 
+    @Override
+    public List<CrmStatisticsCustomerDealCycleByAreaRespVO> getCustomerDealCycleByArea(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        List<Long> userIds = getUserIds(reqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        reqVO.setUserIds(userIds);
+
+        // 2. 获取客户地区统计数据
+        List<CrmStatisticsCustomerDealCycleByAreaRespVO> dealCycleByAreaList = customerMapper.selectCustomerDealCycleGroupByAreaId(reqVO);
+        if (CollUtil.isEmpty(dealCycleByAreaList)) {
+            return Collections.emptyList();
+        }
+
+        // 3. 拼接数据
+        Map<Integer, Area> areaMap = convertMap(AreaUtils.getByType(AreaTypeEnum.PROVINCE, Function.identity()), Area::getId);
+        return convertList(dealCycleByAreaList, vo -> {
+            if (vo.getAreaId() != null) {
+                Integer parentId = AreaUtils.getParentIdByType(vo.getAreaId(), AreaTypeEnum.PROVINCE);
+                findAndThen(areaMap, parentId, area -> vo.setAreaId(parentId).setAreaName(area.getName()));
+            }
+            return vo;
+        });
+    }
+
+    @Override
+    public List<CrmStatisticsCustomerDealCycleByProductRespVO> getCustomerDealCycleByProduct(CrmStatisticsCustomerReqVO reqVO) {
+        // 1. 获得用户编号数组
+        List<Long> userIds = getUserIds(reqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        reqVO.setUserIds(userIds);
+
+        // 2. 获取客户产品统计数据
+        // TODO @dhb52:未读取产品名
+        return customerMapper.selectCustomerDealCycleGroupByProductId(reqVO);
+    }
+
     /**
      * 拼接用户信息(昵称)
      *

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

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+
+import java.util.List;
+
+/**
+ * CRM 销售漏斗分析 Service
+ *
+ * @author HUIHUI
+ */
+public interface CrmStatisticsFunnelService {
+
+    /**
+     * 获得销售漏斗数据
+     *
+     * @param reqVO 请求
+     * @return 销售漏斗数据
+     */
+    CrmStatisticFunnelSummaryRespVO getFunnelSummary(CrmStatisticsFunnelReqVO reqVO);
+
+    /**
+     * 获得商机结束状态统计
+     *
+     * @param reqVO 请求
+     * @return 商机结束状态统计
+     */
+    List<CrmStatisticsBusinessSummaryByEndStatusRespVO> getBusinessSummaryByEndStatus(CrmStatisticsFunnelReqVO reqVO);
+
+    /**
+     * 获取新增商机分析(按日期)
+     *
+     * @param reqVO 请求
+     * @return 新增商机分析
+     */
+    List<CrmStatisticsBusinessSummaryByDateRespVO> getBusinessSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
+
+    /**
+     * 获得商机转化率分析(按日期)
+     *
+     * @param reqVO 请求
+     * @return 商机转化率分析
+     */
+    List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO> getBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
+
+    /**
+     * 获得商机分页(按日期)
+     *
+     * @param pageVO 请求
+     * @return 商机分页
+     */
+    PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO);
+
+}

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

@@ -0,0 +1,153 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsFunnelMapper;
+import cn.iocoder.yudao.module.crm.enums.business.CrmBusinessEndStatusEnum;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * CRM 销售漏斗分析 Service 实现类
+ *
+ * @author HUIHUI
+ */
+@Service
+public class CrmStatisticsFunnelServiceImpl implements CrmStatisticsFunnelService {
+
+    @Resource
+    private CrmStatisticsFunnelMapper funnelMapper;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private CrmBusinessService businessService;
+    @Resource
+    private DeptApi deptApi;
+
+    @Override
+    public CrmStatisticFunnelSummaryRespVO getFunnelSummary(CrmStatisticsFunnelReqVO reqVO) {
+        // 1. 获得用户编号数组
+        List<Long> userIds = getUserIds(reqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return null;
+        }
+        reqVO.setUserIds(userIds);
+
+        // 2. 获得漏斗数据
+        Long customerCount = funnelMapper.selectCustomerCountByDate(reqVO);
+        Long businessCount = funnelMapper.selectBusinessCountByDateAndEndStatus(reqVO, null);
+        Long businessWinCount = funnelMapper.selectBusinessCountByDateAndEndStatus(reqVO, CrmBusinessEndStatusEnum.WIN.getStatus());
+        return new CrmStatisticFunnelSummaryRespVO(customerCount, businessCount, businessWinCount);
+    }
+
+    @Override
+    public List<CrmStatisticsBusinessSummaryByEndStatusRespVO> getBusinessSummaryByEndStatus(CrmStatisticsFunnelReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 获得统计数据
+        return funnelMapper.selectBusinessSummaryListGroupByEndStatus(reqVO);
+    }
+
+    @Override
+    public List<CrmStatisticsBusinessSummaryByDateRespVO> getBusinessSummaryByDate(CrmStatisticsFunnelReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按天统计,获取分项统计数据
+        List<CrmStatisticsBusinessSummaryByDateRespVO> businessSummaryList = funnelMapper.selectBusinessSummaryGroupByDate(reqVO);
+        // 3. 按照日期间隔,合并数据
+        List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+        return convertList(timeRanges, times -> {
+            Long businessCreateCount = businessSummaryList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToLong(CrmStatisticsBusinessSummaryByDateRespVO::getBusinessCreateCount).sum();
+            BigDecimal businessDealCount = businessSummaryList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .map(CrmStatisticsBusinessSummaryByDateRespVO::getTotalPrice)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            return new CrmStatisticsBusinessSummaryByDateRespVO()
+                    .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+                    .setBusinessCreateCount(businessCreateCount).setTotalPrice(businessDealCount);
+        });
+    }
+
+    @Override
+    public List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO> getBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按天统计,获取分项统计数据
+        List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO> businessSummaryList = funnelMapper.selectBusinessInversionRateSummaryByDate(reqVO);
+        // 3. 按照日期间隔,合并数据
+        List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+        return convertList(timeRanges, times -> {
+            Long businessCount = businessSummaryList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToLong(CrmStatisticsBusinessInversionRateSummaryByDateRespVO::getBusinessCount).sum();
+            Long businessWinCount = businessSummaryList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToLong(CrmStatisticsBusinessInversionRateSummaryByDateRespVO::getBusinessWinCount).sum();
+            return new CrmStatisticsBusinessInversionRateSummaryByDateRespVO()
+                    .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+                    .setBusinessCount(businessCount).setBusinessWinCount(businessWinCount);
+        });
+    }
+
+    @Override
+    public PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO) {
+        // 1. 获得用户编号数组
+        pageVO.setUserIds(getUserIds(pageVO));
+        if (CollUtil.isEmpty(pageVO.getUserIds())) {
+            return PageResult.empty();
+        }
+        // 2. 执行查询
+        return businessService.getBusinessPageByDate(pageVO);
+    }
+
+    /**
+     * 获取用户编号数组。如果用户编号为空, 则获得部门下的用户编号数组,包括子部门的所有用户编号
+     *
+     * @param reqVO 请求参数
+     * @return 用户编号数组
+     */
+    private List<Long> getUserIds(CrmStatisticsFunnelReqVO reqVO) {
+        // 情况一:选中某个用户
+        if (ObjUtil.isNotNull(reqVO.getUserId())) {
+            return List.of(reqVO.getUserId());
+        }
+        // 情况二:选中某个部门
+        // 2.1 获得部门列表
+        List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
+        deptIds.add(reqVO.getDeptId());
+        // 2.2 获得用户编号
+        return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
+    }
+
+}

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

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.crm.service.statistics;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
@@ -13,8 +14,9 @@ import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import jakarta.annotation.Resource;
-import java.util.Collections;
-import java.util.List;
+
+import java.math.BigDecimal;
+import java.util.*;
 import java.util.function.Function;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -38,7 +40,7 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
 
     @Override
     public List<CrmStatisticsPerformanceRespVO> getContractCountPerformance(CrmStatisticsPerformanceReqVO performanceReqVO) {
-        // TODO @scholar:我们可以换个思路实现,减少数据库的计算量
+        // TODO @scholar:可以把下面这个注释,你理解后,重新整理下,写到 getPerformance 里
         // 比如说,2024 年的合同数据,是不是 2022-12 到 2024-12-31,每个月的统计呢?
         // 理解之后,我们可以数据 group by 年-月,20222-12 到 2024-12-31 的,然后内存在聚合出 CrmStatisticsPerformanceRespVO 这样
         // 这样,我们就可以减少数据库的计算量,提升性能;同时 SQL 也会很简单,开发者理解起来也简单哈;
@@ -55,28 +57,99 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
         return getPerformance(performanceReqVO, performanceMapper::selectReceivablePricePerformance);
     }
 
+    // TODO @scholar:代码注释,应该有 3 个变量哈;
     /**
      * 获得员工业绩数据
      *
      * @param performanceReqVO  参数
-     * @param performanceFunction 排行榜方法
-     * @return 排行版数据
+     * @param performanceFunction 员工业绩统计方法
+     * @return 员工业绩数据
      */
+    // TODO @scholar:下面一行的变量,超过一行了,阅读不美观;可以考虑每一行一个变量;
     private List<CrmStatisticsPerformanceRespVO> getPerformance(CrmStatisticsPerformanceReqVO performanceReqVO, Function<CrmStatisticsPerformanceReqVO,
             List<CrmStatisticsPerformanceRespVO>> performanceFunction) {
 
+        // TODO @scholar:没使用到的变量,建议删除;
+        List<CrmStatisticsPerformanceRespVO> performanceRespVOList;
+
         // 1. 获得用户编号数组
         final List<Long> userIds = getUserIds(performanceReqVO);
         if (CollUtil.isEmpty(userIds)) {
             return Collections.emptyList();
         }
         performanceReqVO.setUserIds(userIds);
-        // 2. 获得排行数据
+        // TODO @scholar:1. 和 2. 之间,可以考虑换一行;保证每一块逻辑的间隔;
+        // 2. 获得业绩数据
+        // TODO @scholar:复数变量,建议使用 s 或者 list 结果;这里用 performanceList  好列;
         List<CrmStatisticsPerformanceRespVO> performance = performanceFunction.apply(performanceReqVO);
-        if (CollUtil.isEmpty(performance)) {
-            return Collections.emptyList();
+
+        // 获取查询的年份
+        // TODO @scholar:逻辑可以简化一下;
+        // TODO 1)把 performance 转换成 map;key 是 time,value 是 count
+        // TODO 2)当前年,遍历 1-12 月份,去 map 拿到 count;接着月份 -1,去 map 拿 count;再年份 -1,拿 count
+        String currentYear = LocalDateTimeUtil.format(performanceReqVO.getTimes()[0],"yyyy");
+
+        // 构造查询当年和前一年,每年12个月的年月组合
+        List<String> allMonths = new ArrayList<>();
+        for (int year = Integer.parseInt(currentYear)-1; year <= Integer.parseInt(currentYear); year++) {
+            for (int month = 1; month <= 12; month++) {
+                allMonths.add(String.format("%d%02d", year, month));
+            }
+        }
+
+        List<CrmStatisticsPerformanceRespVO> computedList = new ArrayList<>();
+        List<CrmStatisticsPerformanceRespVO> respVOList = new ArrayList<>();
+
+        // 生成computedList基础数据
+        // 构造完整的2*12个月的数据,如果某月数据缺失,需要补上0,一年12个月不能有缺失
+        for (String month : allMonths) {
+            CrmStatisticsPerformanceRespVO foundData = performance.stream()
+                    .filter(data -> data.getTime().equals(month))
+                    .findFirst()
+                    .orElse(null);
+
+            if (foundData != null) {
+                computedList.add(foundData);
+            } else {
+                CrmStatisticsPerformanceRespVO missingData = new CrmStatisticsPerformanceRespVO();
+                missingData.setTime(month);
+                missingData.setCurrentMonthCount(BigDecimal.ZERO);
+                missingData.setLastMonthCount(BigDecimal.ZERO);
+                missingData.setLastYearCount(BigDecimal.ZERO);
+                computedList.add(missingData);
+            }
+        }
+        //根据查询年份和前一年的数据,计算查询年份的同比环比数据
+        for (CrmStatisticsPerformanceRespVO currentData : computedList) {
+            String currentMonth = currentData.getTime();
+
+            // 根据当年和前一年的月销售数据,计算currentYear的完整数据
+            if (currentMonth.startsWith(currentYear)) {
+                // 计算 LastMonthCount
+                int currentIndex = computedList.indexOf(currentData);
+                if (currentIndex > 0) {
+                    CrmStatisticsPerformanceRespVO lastMonthData = computedList.get(currentIndex - 1);
+                    currentData.setLastMonthCount(lastMonthData.getCurrentMonthCount());
+                } else {
+                    currentData.setLastMonthCount(BigDecimal.ZERO); // 第一个月的 LastMonthCount 设为0
+                }
+
+                // 计算 LastYearCount
+                String lastYearMonth = String.valueOf(Integer.parseInt(currentMonth) - 100);
+                CrmStatisticsPerformanceRespVO lastYearData = computedList.stream()
+                        .filter(data -> data.getTime().equals(lastYearMonth))
+                        .findFirst()
+                        .orElse(null);
+
+                if (lastYearData != null) {
+                    currentData.setLastYearCount(lastYearData.getCurrentMonthCount());
+                } else {
+                    currentData.setLastYearCount(BigDecimal.ZERO); // 如果去年同月数据不存在,设为0
+                }
+                respVOList.add(currentData);//给前端只需要返回查询当年的数据,不需要前一年数据
+            }
         }
-        return performance;
+        return respVOList;
     }
 
     /**

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

@@ -20,7 +20,6 @@ import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
-import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 
 /**
  * CRM 客户画像 Service 实现类
@@ -55,15 +54,18 @@ public class CrmStatisticsPortraitServiceImpl implements CrmStatisticsPortraitSe
 
         // 3. 拼接数据
         List<Area> areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
-        areaList.add(new Area().setId(null).setName("未知")); // TODO @puhui999:是不是 65 find 的逻辑改下;不用 findAndThen,直接从 areaMap 拿;拿到就设置,不拿到就设置 null 和 未知;这样,58 本行可以删除掉完事了;这样代码更简单和一致
         Map<Integer, Area> areaMap = convertMap(areaList, Area::getId);
         return convertList(list, item -> {
             Integer parentId = AreaUtils.getParentIdByType(item.getAreaId(), AreaTypeEnum.PROVINCE);
-            if (parentId == null) { // 找不到,归到未知
-                return item.setAreaId(null).setAreaName("未知");
+            if (parentId != null) {
+                Area area = areaMap.get(parentId);
+                if (area != null) {
+                    item.setAreaId(parentId).setAreaName(area.getName());
+                    return item;
+                }
             }
-            findAndThen(areaMap, parentId, area -> item.setAreaId(parentId).setAreaName(area.getName()));
-            return item;
+            // 找不到,归到未知
+            return item.setAreaId(null).setAreaName("未知");
         });
     }
 

+ 4 - 2
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java

@@ -64,8 +64,10 @@ public class CrmPermissionUtils {
         }
         // 2.2 场景二:我参与的数据
         if (CrmSceneTypeEnum.isInvolved(sceneType)) {
-            query.ne(ownerUserIdField, userId)
-                    .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel());
+            query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType)
+                    .eq(CrmPermissionDO::getBizId, bizId)
+                    .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel()));
+            query.ne(ownerUserIdField, userId);
         }
         // 2.3 场景三:下属负责的数据
         if (CrmSceneTypeEnum.isSubordinate(sceneType)) {

+ 49 - 7
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml

@@ -16,20 +16,20 @@
          GROUP BY time
     </select>
 
-    <!-- TODO 芋艿:应该不用过滤时间 -->
     <select id="selectCustomerDealCountGroupByDate"
             resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO">
-        SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time,
-               COUNT(DISTINCT customer.id) AS customer_deal_count
+        SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d')    AS time,
+               COUNT(DISTINCT customer.id)                      AS customer_deal_count
           FROM crm_customer AS customer
                 LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
-         WHERE customer.deleted = 0 AND contract.deleted = 0
+         WHERE customer.deleted = 0
+           AND contract.deleted = 0
            AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
            AND customer.owner_user_id IN
                 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
                     #{userId}
                 </foreach>
-           AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+           AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
          GROUP BY time
     </select>
 
@@ -53,13 +53,14 @@
                COUNT(DISTINCT customer.id) AS customer_deal_count
           FROM crm_customer AS customer
                 LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
-         WHERE customer.deleted = 0 AND contract.deleted = 0
+         WHERE customer.deleted = 0
+           AND contract.deleted = 0
            AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
            AND customer.owner_user_id IN
                 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
                     #{userId}
                 </foreach>
-           AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+           AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
          GROUP BY customer.owner_user_id
     </select>
 
@@ -221,4 +222,45 @@
          GROUP BY customer.owner_user_id
     </select>
 
+    <select id="selectCustomerDealCycleGroupByAreaId"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByAreaRespVO">
+        SELECT customer.area_id AS area_id,
+               IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle,
+               COUNT(DISTINCT customer.id) AS customer_deal_count
+        FROM crm_customer AS customer
+                LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id
+        WHERE customer.deleted = 0
+          AND contract.deleted = 0
+          AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+          AND customer.owner_user_id IN
+                <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                    #{userId}
+                </foreach>
+          AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY
+            customer.area_id
+    </select>
+
+    <select id="selectCustomerDealCycleGroupByProductId"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByProductRespVO">
+        SELECT product.name                                                                                 AS product_name,
+               IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0)   AS customer_deal_cycle,
+               COUNT(DISTINCT customer.id)                                                                  AS customer_deal_count
+          FROM crm_customer AS customer
+                LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id
+                LEFT JOIN crm_contract_product AS contract_product ON contract_product.contract_id = contract.id
+                LEFT JOIN crm_product AS product ON contract_product.product_id = product.id
+         WHERE customer.deleted = 0
+           AND contract.deleted = 0
+           AND contract_product.deleted = 0
+           AND product.deleted = 0
+           AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
+           AND customer.owner_user_id IN
+                 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                     #{userId}
+                 </foreach>
+           AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY product.id
+    </select>
+
 </mapper>

+ 87 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsFunnelMapper">
+
+    <select id="selectCustomerCountByDate" resultType="java.lang.Long">
+        SELECT
+            COUNT(*)
+        FROM crm_customer
+        WHERE deleted = 0
+        AND owner_user_id IN
+        <!-- TODO @puhui999:这个 foreach 搞个缩进哈 - -->
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
+        <!-- TODO @puhui999:下面这个,就不缩进啦 - -->
+        #{times[1],javaType=java.time.LocalDateTime}
+    </select>
+
+    <select id="selectBusinessCountByDateAndEndStatus" resultType="java.lang.Long">
+        SELECT
+            COUNT(*)
+        FROM crm_business
+        WHERE deleted = 0
+        <if test="status != null">
+            AND end_status = #{status}
+        </if>
+        AND owner_user_id IN
+        <foreach collection="reqVO.userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{reqVO.times[0],javaType=java.time.LocalDateTime} AND
+        #{reqVO.times[1],javaType=java.time.LocalDateTime}
+    </select>
+
+    <select id="selectBusinessSummaryListGroupByEndStatus"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByEndStatusRespVO">
+        SELECT
+            end_status AS endStatus,
+            COUNT(*) AS businessCount,
+            SUM(total_price) AS totalPrice
+        FROM crm_business
+        WHERE deleted = 0 AND end_status IS NOT NULL
+        AND owner_user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
+        #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY end_status
+    </select>
+
+    <select id="selectBusinessSummaryGroupByDate"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO">
+        SELECT
+            DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
+            COUNT(*) AS businessCreateCount,
+            SUM(total_price) AS totalPrice
+        FROM crm_business
+        WHERE deleted = 0
+        AND owner_user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
+        #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY time
+    </select>
+
+    <select id="selectBusinessInversionRateSummaryByDate"
+            resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessInversionRateSummaryByDateRespVO">
+        SELECT
+            DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
+            COUNT(*) AS businessCount,
+            SUM(IF(end_status = 1, 1, 0)) AS businessWinCount
+        FROM crm_business
+        WHERE deleted = 0
+        AND owner_user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+        AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
+        #{times[1],javaType=java.time.LocalDateTime}
+        GROUP BY time
+    </select>
+
+</mapper>

+ 20 - 112
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml

@@ -5,75 +5,28 @@
     <select id="selectContractCountPerformance"
             resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
         SELECT
-        t.time as time,
-        COALESCE(t.currentMonthCount,0) as currentMonthCount,
-        COALESCE(y.lastMonthCount,0) as lastMonthCount,
-        COALESCE(z.lastYearCount,0) as lastYearCount
-        FROM
-        (SELECT
-        COUNT(1) AS currentMonthCount,
-        DATE_FORMAT(order_date, '%Y-%m') AS time
-        FROM	crm_contract
+            DATE_FORMAT(order_date, '%Y%m') AS time,
+            COUNT(1) AS currentMonthCount
+        FROM crm_contract
         WHERE deleted = 0
+        <!-- TODO @scholar:20 改成静态类引入 -->
         AND audit_status = 20
         AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
-        GROUP BY time)t
-        LEFT JOIN
-        (SELECT
-        COUNT(1) AS lastMonthCount,
-        DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 MONTH), '%Y-%m') AS time
-        FROM	crm_contract
-        WHERE deleted = 0
-        AND audit_status = 20
-        AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
-        or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
-        GROUP BY time)y ON t.time = y.time
-        LEFT JOIN
-        (SELECT
-        COUNT(1) AS lastYearCount,
-        DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 YEAR), '%Y-%m') AS time
-        FROM	crm_contract
-        WHERE deleted = 0
-        AND audit_status = 20
-        AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
-        GROUP BY time)z ON t.time = z.time
+            <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+                #{userId}
+            </foreach>
+        <!-- TODO @scholar:CrmStatisticsPerformanceReqVO 传递 year,然后 java 代码里,转换出 times;这样,order_time 使用范围查询,避免使用函数 -->
+        AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime}, '%Y')
+        or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime}, '%Y') - 1)
+        GROUP BY time
     </select>
+
+    <!-- TODO @scholar:参考上面,调整下这个 SQL 的排版、和代码建议哈 -->
     <select id="selectContractPricePerformance"
             resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
         SELECT
-        t.time as time,
-        COALESCE(t.currentMonthCount,0) as currentMonthCount,
-        COALESCE(y.lastMonthCount,0) as lastMonthCount,
-        COALESCE(z.lastYearCount,0) as lastYearCount
-        FROM
-        (SELECT
-        IFNULL(SUM(total_price), 0) AS currentMonthCount,
-        DATE_FORMAT(order_date, '%Y-%m') AS time
-        FROM	crm_contract
-        WHERE deleted = 0
-        AND audit_status = 20
-        AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
-        GROUP BY time)t
-        LEFT JOIN
-        (SELECT
-        IFNULL(SUM(total_price), 0) AS lastMonthCount,
-        DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 MONTH), '%Y-%m') AS time
+        DATE_FORMAT(order_date, '%Y%m') AS time,
+        IFNULL(SUM(total_price), 0) AS currentMonthCount
         FROM	crm_contract
         WHERE deleted = 0
         AND audit_status = 20
@@ -83,46 +36,15 @@
         </foreach>
         AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
         or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
-        GROUP BY time)y ON t.time = y.time
-        LEFT JOIN
-        (SELECT
-        IFNULL(SUM(total_price), 0) AS lastYearCount,
-        DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 YEAR), '%Y-%m') AS time
-        FROM	crm_contract
-        WHERE deleted = 0
-        AND audit_status = 20
-        AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
-        GROUP BY time)z ON t.time = z.time
+        GROUP BY time
     </select>
 
+    <!-- TODO @scholar:参考上面,调整下这个 SQL 的排版、和代码建议哈 -->
     <select id="selectReceivablePricePerformance"
             resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
         SELECT
-        t.time as time,
-        COALESCE(t.currentMonthCount,0) as currentMonthCount,
-        COALESCE(y.lastMonthCount,0) as lastMonthCount,
-        COALESCE(z.lastYearCount,0) as lastYearCount
-        FROM
-        (SELECT
-        IFNULL(SUM(price), 0) AS currentMonthCount,
-        DATE_FORMAT(return_time, '%Y-%m') AS time
-        FROM	crm_receivable
-        WHERE deleted = 0
-        AND audit_status = 20
-        AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
-        GROUP BY time)t
-        LEFT JOIN
-        (SELECT
-        IFNULL(SUM(price), 0) AS lastMonthCount,
-        DATE_FORMAT(DATE_ADD(return_time,INTERVAL 1 MONTH), '%Y-%m') AS time
+        DATE_FORMAT(return_time, '%Y%m') AS time,
+        IFNULL(SUM(price), 0) AS currentMonthCount
         FROM	crm_receivable
         WHERE deleted = 0
         AND audit_status = 20
@@ -132,21 +54,7 @@
         </foreach>
         AND (DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
         or DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
-        GROUP BY time)y ON t.time = y.time
-        LEFT JOIN
-        (SELECT
-        IFNULL(SUM(price), 0) AS lastYearCount,
-        DATE_FORMAT(DATE_ADD(return_time,INTERVAL 1 YEAR), '%Y-%m') AS time
-        FROM	crm_receivable
-        WHERE deleted = 0
-        AND audit_status = 20
-        AND owner_user_id in
-        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
-            #{userId}
-        </foreach>
-        AND DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
-        GROUP BY time)z ON t.time = z.time
+        GROUP BY time
     </select>
 
-
 </mapper>

+ 1 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java

@@ -113,7 +113,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
 
         // 更新可见状态
         productCommentMapper.updateById(new ProductCommentDO().setId(updateReqVO.getId())
-                .setVisible(true));
+                .setVisible(updateReqVO.getVisible()));
     }
 
     @Override

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java

@@ -88,7 +88,7 @@ public class DeliveryExpressTemplateServiceImpl implements DeliveryExpressTempla
         List<DeliveryExpressTemplateFreeDO> oldList = expressTemplateFreeMapper.selectListByTemplateId(templateId);
         List<DeliveryExpressTemplateFreeDO> newList = INSTANCE.convertTemplateFreeList(templateId, frees);
         List<List<DeliveryExpressTemplateFreeDO>> diffList = CollectionUtils.diffList(oldList, newList,
-                (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getTemplateId()));
+                (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId()));
 
         // 第二步,批量添加、修改、删除
         if (CollUtil.isNotEmpty(diffList.get(0))) {

+ 4 - 2
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java

@@ -36,13 +36,15 @@ public class PayWalletServiceImpl implements  PayWalletService {
 
     @Resource
     private PayWalletMapper walletMapper;
+
     @Resource
+    @Lazy // 延迟加载,避免循环依赖
     private PayWalletTransactionService walletTransactionService;
     @Resource
-    @Lazy
+    @Lazy // 延迟加载,避免循环依赖
     private PayOrderService orderService;
     @Resource
-    @Lazy
+    @Lazy // 延迟加载,避免循环依赖
     private PayRefundService refundService;
 
     @Override

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java

@@ -18,7 +18,7 @@ public class DeptRespVO {
     @Schema(description = "父部门 ID", example = "1024")
     private Long parentId;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Integer sort;
 
     @Schema(description = "负责人的用户编号", example = "2048")

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java

@@ -25,7 +25,7 @@ public class DeptSaveReqVO {
     @Schema(description = "父部门 ID", example = "1024")
     private Long parentId;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "显示顺序不能为空")
     private Integer sort;
 

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java

@@ -27,7 +27,7 @@ public class PostRespVO {
     @ExcelProperty("岗位编码")
     private String code;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @ExcelProperty("岗位排序")
     private Integer sort;
 

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java

@@ -26,7 +26,7 @@ public class PostSaveReqVO {
     @Size(max = 64, message = "岗位编码长度不能超过64个字符")
     private String code;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "显示顺序不能为空")
     private Integer sort;
 

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java

@@ -19,7 +19,7 @@ public class DictDataRespVO {
     @ExcelProperty("字典编码")
     private Long id;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @ExcelProperty("字典排序")
     private Integer sort;
 

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java

@@ -16,7 +16,7 @@ public class DictDataSaveReqVO {
     @Schema(description = "字典数据编号", example = "1024")
     private Long id;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "显示顺序不能为空")
     private Integer sort;
 

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java

@@ -28,7 +28,7 @@ public class MenuRespVO {
     @NotNull(message = "菜单类型不能为空")
     private Integer type;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "显示顺序不能为空")
     private Integer sort;
 

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java

@@ -27,7 +27,7 @@ public class MenuSaveVO {
     @NotNull(message = "菜单类型不能为空")
     private Integer type;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "显示顺序不能为空")
     private Integer sort;
 

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java

@@ -30,7 +30,7 @@ public class RoleRespVO {
     @ExcelProperty("角色标志")
     private String code;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @ExcelProperty("角色排序")
     private Integer sort;
 

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java

@@ -27,7 +27,7 @@ public class RoleSaveReqVO {
     @DiffLogField(name = "角色标志")
     private String code;
 
-    @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "显示顺序不能为空")
     @DiffLogField(name = "显示顺序")
     private Integer sort;

+ 7 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java

@@ -2,14 +2,16 @@ package cn.iocoder.yudao.module.system.service.permission;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
 import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper;
 import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
 import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
 import cn.iocoder.yudao.module.system.service.tenant.TenantService;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.cache.annotation.Cacheable;
@@ -17,7 +19,6 @@ import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import jakarta.annotation.Resource;
 import java.util.Collection;
 import java.util.List;
 
@@ -130,6 +131,10 @@ public class MenuServiceImpl implements MenuService {
 
     @Override
     public List<MenuDO> getMenuList(Collection<Long> ids) {
+        // 当 ids 为空时,返回一个空的实例对象
+        if (CollUtil.isEmpty(ids)) {
+            return Lists.newArrayList();
+        }
         return menuMapper.selectBatchIds(ids);
     }
 

+ 10 - 0
yudao-server/src/main/resources/application-dev.yaml

@@ -188,6 +188,16 @@ justauth:
       client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
       agent-id: 1000004
       ignore-check-redirect-uri: true
+    # noinspection SpringBootApplicationYaml
+    WECHAT_MINI_APP: # 微信小程序
+      client-id: ${wx.miniapp.appid}
+      client-secret: ${wx.miniapp.secret}
+      ignore-check-redirect-uri: true
+      ignore-check-state: true # 微信小程序,不会使用到 state,所以不进行校验
+    WECHAT_MP: # 微信公众号
+      client-id: ${wx.mp.app-id}
+      client-secret: ${wx.mp.secret}
+      ignore-check-redirect-uri: true
   cache:
     type: REDIS
     prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::

+ 1 - 0
yudao-server/src/main/resources/application-local.yaml

@@ -242,6 +242,7 @@ justauth:
       client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
       agent-id: 1000004
       ignore-check-redirect-uri: true
+    # noinspection SpringBootApplicationYaml
     WECHAT_MINI_APP: # 微信小程序
       client-id: ${wx.miniapp.appid}
       client-secret: ${wx.miniapp.secret}

Неке датотеке нису приказане због велике количине промена