瀏覽代碼

!946 CRM: 完善销售漏斗分析
Merge pull request !946 from puhui999/develop

芋道源码 1 年之前
父節點
當前提交
0eff2ae602
共有 18 個文件被更改,包括 746 次插入14 次删除
  1. 4 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  2. 130 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsFunnelController.java
  3. 25 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticBusinessEndStatusRespVO.java
  4. 23 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticFunnelRespVO.java
  5. 109 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessRespVO.java
  6. 21 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByDateRespVO.java
  7. 53 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsFunnelReqVO.java
  8. 29 2
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
  9. 8 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
  10. 20 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsFunnelMapper.java
  11. 38 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
  12. 30 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
  13. 15 6
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
  14. 9 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
  15. 51 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelService.java
  16. 154 0
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelServiceImpl.java
  17. 8 6
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitServiceImpl.java
  18. 19 0
      yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml

+ 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<>();

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

@@ -0,0 +1,130 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticBusinessEndStatusRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticFunnelRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO;
+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.CrmBusinessStatusDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusTypeDO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessStatusService;
+import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsFunnelService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.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.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "管理后台 - CRM 销售漏斗")
+@RestController
+@RequestMapping("/crm/statistics-funnel")
+@Validated
+public class CrmStatisticsFunnelController {
+
+    @Resource
+    private CrmStatisticsFunnelService crmStatisticsFunnelService;
+    @Resource
+    private CrmBusinessService businessService;
+    @Resource
+    private CrmCustomerService customerService;
+    @Resource
+    private CrmBusinessStatusService businessStatusTypeService;
+    @Resource
+    private CrmBusinessStatusService businessStatusService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
+
+    @GetMapping("/get-funnel-summary")
+    @Operation(summary = "获取销售漏斗统计数据", description = "用于【销售漏斗】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+    public CommonResult<CrmStatisticFunnelRespVO> getFunnelSummary(@Valid CrmStatisticsFunnelReqVO reqVO) {
+        return success(crmStatisticsFunnelService.getFunnelSummary(reqVO));
+    }
+
+    @GetMapping("/get-business-end-status-summary")
+    @Operation(summary = "获取商机结束状态统计", description = "用于【销售漏斗】页面")
+    @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+    public CommonResult<List<CrmStatisticBusinessEndStatusRespVO>> getBusinessEndStatusSummary(@Valid CrmStatisticsFunnelReqVO reqVO) {
+        return success(crmStatisticsFunnelService.getBusinessEndStatusSummary(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(crmStatisticsFunnelService.getBusinessSummaryByDate(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 = crmStatisticsFunnelService.getBusinessPageByDate(pageVO);
+        return success(new PageResult<>(buildBusinessDetailList(pageResult.getList()), pageResult.getTotal()));
+    }
+
+    private List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
+        if (CollUtil.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        // 1.1 获取客户列表
+        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
+                convertSet(list, CrmBusinessDO::getCustomerId));
+        // 1.2 获取创建人、负责人列表
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(list,
+                contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getOwnerUserId())));
+        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
+        // 1.3 获得商机状态组
+        Map<Long, CrmBusinessStatusTypeDO> statusTypeMap = businessStatusTypeService.getBusinessStatusTypeMap(
+                convertSet(list, CrmBusinessDO::getStatusTypeId));
+        Map<Long, CrmBusinessStatusDO> statusMap = businessStatusService.getBusinessStatusMap(
+                convertSet(list, CrmBusinessDO::getStatusId));
+        // 2. 拼接数据
+        return BeanUtils.toBean(list, CrmBusinessRespVO.class, businessVO -> {
+            // 2.1 设置客户名称
+            MapUtils.findAndThen(customerMap, businessVO.getCustomerId(), customer -> businessVO.setCustomerName(customer.getName()));
+            // 2.2 设置创建人、负责人名称
+            MapUtils.findAndThen(userMap, NumberUtils.parseLong(businessVO.getCreator()),
+                    user -> businessVO.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, businessVO.getOwnerUserId(), user -> {
+                businessVO.setOwnerUserName(user.getNickname());
+                MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> businessVO.setOwnerUserDeptName(dept.getName()));
+            });
+            // 2.3 设置商机状态
+            MapUtils.findAndThen(statusTypeMap, businessVO.getStatusTypeId(), statusType -> businessVO.setStatusTypeName(statusType.getName()));
+            MapUtils.findAndThen(statusMap, businessVO.getStatusId(), status -> businessVO.setStatusName(
+                    businessService.getBusinessStatusName(businessVO.getEndStatus(), status)));
+        });
+    }
+
+}

+ 25 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticBusinessEndStatusRespVO.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 CrmStatisticBusinessEndStatusRespVO {
+
+    @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;
+
+}

+ 23 - 0
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticFunnelRespVO.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 CrmStatisticFunnelRespVO {
+
+    @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 winCount;
+
+}

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

@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - CRM 商机 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class CrmStatisticsBusinessRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32129")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "商机名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @ExcelProperty("商机名称")
+    private String name;
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10299")
+    private Long customerId;
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @ExcelProperty("客户名称")
+    private String customerName;
+
+    @Schema(description = "跟进状态", requiredMode = Schema.RequiredMode.REQUIRED, example ="true")
+    @ExcelProperty("跟进状态")
+    private Boolean followUpStatus;
+
+    @Schema(description = "最后跟进时间")
+    @ExcelProperty("最后跟进时间")
+    private LocalDateTime contactLastTime;
+
+    @Schema(description = "下次联系时间")
+    @ExcelProperty("下次联系时间")
+    private LocalDateTime contactNextTime;
+
+    @Schema(description = "负责人的用户编号", example = "25682")
+    @ExcelProperty("负责人的用户编号")
+    private Long ownerUserId;
+    @Schema(description = "负责人名字", example = "25682")
+    @ExcelProperty("负责人名字")
+    private String ownerUserName;
+    @Schema(description = "负责人部门")
+    @ExcelProperty("负责人部门")
+    private String ownerUserDeptName;
+
+    @Schema(description = "商机状态组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25714")
+    private Long statusTypeId;
+    @Schema(description = "商机状组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中")
+    @ExcelProperty("商机状态组")
+    private String statusTypeName;
+
+    @Schema(description = "商机状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30320")
+    private Long statusId;
+    @Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "跟进中")
+    @ExcelProperty("商机状态")
+    private String statusName;
+
+    @Schema
+    @ExcelProperty("结束状态")
+    private Integer endStatus;
+
+    @ExcelProperty("结束时的备注")
+    private String endRemark;
+
+    @Schema(description = "预计成交日期")
+    @ExcelProperty("预计成交日期")
+    private LocalDateTime dealTime;
+
+    @Schema(description = "产品总金额", example = "12025")
+    @ExcelProperty("产品总金额")
+    private BigDecimal totalProductPrice;
+
+    @Schema(description = "整单折扣")
+    @ExcelProperty("整单折扣")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "商机总金额", example = "12371")
+    @ExcelProperty("商机总金额")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "备注", example = "随便")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "创建人", example = "1024")
+    @ExcelProperty("创建人")
+    private String creator;
+    @Schema(description = "创建人名字", example = "芋道源码")
+    @ExcelProperty("创建人名字")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("更新时间")
+    private LocalDateTime updateTime;
+
+}

+ 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 Integer businessCreateCount;
+
+    @Schema(description = "新增商机金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private BigDecimal businessDealCount;
+
+}

+ 53 - 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,53 @@
+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 lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+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
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+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;
+
+    /**
+     * 前端如果选择自定义时间, 那么前端传递起始-终止时间, 如果选择其他时间间隔类型, 则由后台计算起始-终止时间
+     * 并作为参数传递给Mapper
+     */
+    @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] times;
+
+}

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

@@ -1,17 +1,18 @@
 package cn.iocoder.yudao.module.crm.dal.mysql.business;
 
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
 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.business.vo.business.CrmBusinessPageReqVO;
 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
 import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
 
@@ -59,10 +60,36 @@ 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 Long selectCountByOwnerUserIdsAndEndStatus(Collection<Long> ownerUserIds, LocalDateTime[] times, Integer endStatus) {
+        return selectCount(new LambdaQueryWrapperX<CrmBusinessDO>()
+                .in(CrmBusinessDO::getOwnerUserId, ownerUserIds)
+                .eqIfPresent(CrmBusinessDO::getEndStatus, endStatus)
+                .betweenIfPresent(CrmBusinessDO::getCreateTime, times));
+    }
+
+    default List<CrmBusinessDO> selectListByOwnerUserIdsAndEndStatusNotNull(Collection<Long> ownerUserIds, LocalDateTime[] times) {
+        return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
+                .in(CrmBusinessDO::getOwnerUserId, ownerUserIds)
+                .betweenIfPresent(CrmBusinessDO::getCreateTime, times)
+                .isNotNull(CrmBusinessDO::getEndStatus));
+    }
+
+    default List<CrmBusinessDO> selectListByOwnerUserIdsAndDate(Collection<Long> ownerUserIds, LocalDateTime[] times) {
+        return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
+                .in(CrmBusinessDO::getOwnerUserId, ownerUserIds)
+                .betweenIfPresent(CrmBusinessDO::getCreateTime, times));
+    }
+
+    default PageResult<CrmBusinessDO> selectPage(Collection<Long> ownerUserIds, LocalDateTime[] times, Integer pageNo, Integer pageSize) {
+        return selectPage(new PageParam().setPageNo(pageNo).setPageSize(pageSize), new LambdaQueryWrapperX<CrmBusinessDO>()
+                .in(CrmBusinessDO::getOwnerUserId, ownerUserIds)
+                .betweenIfPresent(CrmBusinessDO::getCreateTime, times));
+    }
+
 }

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

@@ -19,9 +19,11 @@ import org.apache.ibatis.annotations.Mapper;
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
+import javax.management.ObjectName;
 import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 /**
  * 客户 Mapper
@@ -186,4 +188,10 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
         return selectCount(query);
     }
 
+    default Long selectCountByOwnerUserIds(Collection<Long> ownerUserIds, LocalDateTime[] times){
+        return selectCount(new LambdaQueryWrapperX<CrmCustomerDO>()
+                .in(CrmCustomerDO::getOwnerUserId, ownerUserIds)
+                .betweenIfPresent(CrmCustomerDO::getCreateTime, times));
+    }
+
 }

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

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
+
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * CRM 销售漏斗 Mapper
+ *
+ * @author HUIHUI
+ */
+@Mapper
+public interface CrmStatisticsFunnelMapper {
+
+    List<CrmStatisticsBusinessSummaryByDateRespVO> selectBusinessCreateCountGroupByDate(CrmStatisticsFunnelReqVO reqVO);
+
+}

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

@@ -194,4 +194,42 @@ public interface CrmBusinessService {
      */
     List<CrmBusinessDO> getBusinessListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId);
 
+    /**
+     * 获得商机数
+     *
+     * @param ownerUserIds 负责人编号
+     * @param times        时间范围
+     * @param endStatus    商机结束状态
+     * @return 商机数
+     */
+    Long getBusinessCountByOwnerUserIdsAndEndStatus(List<Long> ownerUserIds, LocalDateTime[] times, Integer endStatus);
+
+    /**
+     * 获得商机列表【数据统计】
+     *
+     * @param ownerUserIds 负责人编号
+     * @param times        时间范围
+     * @return 商机列表
+     */
+    List<CrmBusinessDO> getBusinessListByOwnerUserIdsAndEndStatusNotNull(List<Long> ownerUserIds, LocalDateTime[] times);
+
+    /**
+     * 获得商机列表【数据统计】
+     *
+     * @param ownerUserIds 负责人编号
+     * @param times        时间范围
+     * @return 商机列表
+     */
+    List<CrmBusinessDO> getBusinessListByOwnerUserIdsAndDate(List<Long> ownerUserIds, LocalDateTime[] times);
+
+    /**
+     * 商机分页【数据统计】
+     * @param ownerUserIds 负责人编号
+     * @param times        时间范围
+     * @param pageNo       页码
+     * @param pageSize     数量
+     * @return 商机分页
+     */
+    PageResult<CrmBusinessDO> getBusinessPageByDate(List<Long> ownerUserIds, LocalDateTime[] times, Integer pageNo, Integer pageSize);
+
 }

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

@@ -375,4 +375,34 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
         return businessMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
     }
 
+    @Override
+    public Long getBusinessCountByOwnerUserIdsAndEndStatus(List<Long> ownerUserIds, LocalDateTime[] times, Integer endStatus) {
+        if (CollUtil.isEmpty(ownerUserIds)) {
+            return 0L;
+        }
+        return businessMapper.selectCountByOwnerUserIdsAndEndStatus(convertSet(ownerUserIds), times, endStatus);
+    }
+
+    @Override
+    public List<CrmBusinessDO> getBusinessListByOwnerUserIdsAndEndStatusNotNull(List<Long> ownerUserIds, LocalDateTime[] times) {
+        if (CollUtil.isEmpty(ownerUserIds)) {
+            return Collections.emptyList();
+        }
+        return businessMapper.selectListByOwnerUserIdsAndEndStatusNotNull(convertSet(ownerUserIds), times);
+    }
+
+    @Override
+    public List<CrmBusinessDO> getBusinessListByOwnerUserIdsAndDate(List<Long> ownerUserIds, LocalDateTime[] times) {
+        if (CollUtil.isEmpty(ownerUserIds)) {
+            return Collections.emptyList();
+        }
+
+        return businessMapper.selectListByOwnerUserIdsAndDate(convertSet(ownerUserIds), times);
+    }
+
+    @Override
+    public PageResult<CrmBusinessDO> getBusinessPageByDate(List<Long> ownerUserIds, LocalDateTime[] times, Integer pageNo, Integer pageSize) {
+        return businessMapper.selectPage(ownerUserIds, times, pageNo, pageSize);
+    }
+
 }

+ 15 - 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);
@@ -195,4 +195,13 @@ public interface CrmCustomerService {
      */
     int autoPutCustomerPool();
 
+    /**
+     * 获得客户数
+     *
+     * @param ownerUserIds 负责人编号
+     * @param times        时间范围
+     * @return 客户数
+     */
+    Long getCustomerCountByOwnerUserIds(List<Long> ownerUserIds, LocalDateTime[] times);
+
 }

+ 9 - 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.*;
@@ -650,6 +651,14 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
         }
     }
 
+    @Override
+    public Long getCustomerCountByOwnerUserIds(List<Long> ownerUserIds, LocalDateTime[] times) {
+        if (CollUtil.isEmpty(ownerUserIds)) {
+            return 0L;
+        }
+        return customerMapper.selectCountByOwnerUserIds(convertSet(ownerUserIds), times);
+    }
+
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *

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

@@ -0,0 +1,51 @@
+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.CrmStatisticBusinessEndStatusRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticFunnelRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+
+import java.util.List;
+
+/**
+ * CRM 销售漏斗分析 Service
+ *
+ * @author HUIHUI
+ */
+public interface CrmStatisticsFunnelService {
+
+    /**
+     * 获得销售漏斗数据
+     *
+     * @param reqVO 请求
+     * @return 销售漏斗数据
+     */
+    CrmStatisticFunnelRespVO getFunnelSummary(CrmStatisticsFunnelReqVO reqVO);
+
+    /**
+     * 获得商机结束状态统计
+     *
+     * @param reqVO 请求
+     * @return 商机结束状态统计
+     */
+    List<CrmStatisticBusinessEndStatusRespVO> getBusinessEndStatusSummary(CrmStatisticsFunnelReqVO reqVO);
+
+    /**
+     * 获取新增商机分析(按日期)
+     *
+     * @param reqVO 请求
+     * @return 新增商机分析
+     */
+    List<CrmStatisticsBusinessSummaryByDateRespVO> getBusinessSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
+
+    /**
+     * 获得商机分页(按日期)
+     *
+     * @param pageVO 请求
+     * @return 商机分页
+     */
+    PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO);
+
+}

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

@@ -0,0 +1,154 @@
+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.CrmStatisticBusinessEndStatusRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticFunnelRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO;
+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.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.crm.service.customer.CrmCustomerService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
+
+/**
+ * CRM 销售漏斗分析 Service 实现类
+ *
+ * @author HUIHUI
+ */
+@Service
+public class CrmStatisticsFunnelServiceImpl implements CrmStatisticsFunnelService {
+
+    @Resource
+    private CrmStatisticsFunnelMapper funnelMapper;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private CrmCustomerService customerService;
+    @Resource
+    private CrmBusinessService businessService;
+    @Resource
+    private DeptApi deptApi;
+
+    @Override
+    public CrmStatisticFunnelRespVO getFunnelSummary(CrmStatisticsFunnelReqVO reqVO) {
+        // 1. 获得用户编号数组
+        List<Long> userIds = getUserIds(reqVO);
+        if (CollUtil.isEmpty(userIds)) {
+            return null;
+        }
+        reqVO.setUserIds(userIds);
+
+        // 2. 获得漏斗数据
+        return new CrmStatisticFunnelRespVO(
+                customerService.getCustomerCountByOwnerUserIds(userIds, reqVO.getTimes()),
+                businessService.getBusinessCountByOwnerUserIdsAndEndStatus(userIds, reqVO.getTimes(), null),
+                businessService.getBusinessCountByOwnerUserIdsAndEndStatus(userIds, reqVO.getTimes(), CrmBusinessEndStatusEnum.WIN.getStatus())
+        );
+    }
+
+    @Override
+    public List<CrmStatisticBusinessEndStatusRespVO> getBusinessEndStatusSummary(CrmStatisticsFunnelReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2.1 获得用户负责的商机
+        List<CrmBusinessDO> businessList = businessService.getBusinessListByOwnerUserIdsAndEndStatusNotNull(reqVO.getUserIds(), reqVO.getTimes());
+        // 2.2 统计各阶段数据
+        Map<Integer, List<CrmBusinessDO>> businessMap = convertMultiMap(businessList, CrmBusinessDO::getEndStatus);
+        return convertList(CrmBusinessEndStatusEnum.values(), endStatusEnum -> {
+            List<CrmBusinessDO> list = businessMap.get(endStatusEnum.getStatus());
+            if (CollUtil.isEmpty(list)) {
+                return new CrmStatisticBusinessEndStatusRespVO(endStatusEnum.getStatus(), 0L, BigDecimal.ZERO);
+            }
+            return new CrmStatisticBusinessEndStatusRespVO(endStatusEnum.getStatus(), (long) list.size(),
+                    getSumValue(list, CrmBusinessDO::getTotalPrice, BigDecimal::add));
+        });
+    }
+
+    @Override
+    public List<CrmStatisticsBusinessSummaryByDateRespVO> getBusinessSummaryByDate(CrmStatisticsFunnelReqVO reqVO) {
+        // 1. 获得用户编号数组
+        reqVO.setUserIds(getUserIds(reqVO));
+        if (CollUtil.isEmpty(reqVO.getUserIds())) {
+            return Collections.emptyList();
+        }
+
+        // 2. 按天统计,获取分项统计数据
+        List<CrmStatisticsBusinessSummaryByDateRespVO> businessCreateCountList = funnelMapper.selectBusinessCreateCountGroupByDate(reqVO);
+        List<CrmBusinessDO> businessList = businessService.getBusinessListByOwnerUserIdsAndDate(reqVO.getUserIds(), reqVO.getTimes());
+        Map<String, BigDecimal> businessDealCountMap = businessList.stream().collect(Collectors.groupingBy(business ->
+                        business.getCreateTime().format(DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY)),
+                Collectors.reducing(BigDecimal.ZERO, CrmBusinessDO::getTotalPrice, BigDecimal::add)));
+
+        // 3. 按照日期间隔,合并数据
+        List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+        return convertList(timeRanges, times -> {
+            Integer businessCreateCount = businessCreateCountList.stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+                    .mapToInt(CrmStatisticsBusinessSummaryByDateRespVO::getBusinessCreateCount).sum();
+            BigDecimal businessDealCount = businessDealCountMap.entrySet().stream()
+                    .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getKey()))
+                    .map(Map.Entry::getValue)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            return new CrmStatisticsBusinessSummaryByDateRespVO()
+                    .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+                    .setBusinessCreateCount(businessCreateCount).setBusinessDealCount(businessDealCount);
+        });
+    }
+
+    @Override
+    public PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO) {
+        // 1. 获得用户编号数组
+        pageVO.setUserIds(getUserIds(pageVO));
+        if (CollUtil.isEmpty(pageVO.getUserIds())) {
+            return PageResult.empty();
+        }
+
+        return businessService.getBusinessPageByDate(pageVO.getUserIds(), pageVO.getTimes(), pageVO.getPageNo(), pageVO.getPageSize());
+    }
+
+    /**
+     * 获取用户编号数组。如果用户编号为空, 则获得部门下的用户编号数组,包括子部门的所有用户编号
+     *
+     * @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);
+    }
+
+}

+ 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("未知");
         });
     }
 

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

@@ -0,0 +1,19 @@
+<?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="selectBusinessCreateCountGroupByDate"
+            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
+        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>