!946 CRM: 完善销售漏斗分析

Merge pull request !946 from puhui999/develop
This commit is contained in:
芋道源码 2024-04-14 12:23:49 +00:00 committed by Gitee
commit 0eff2ae602
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
18 changed files with 746 additions and 14 deletions

View File

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

View File

@ -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)));
});
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -1,17 +1,18 @@
package cn.iocoder.yudao.module.crm.dal.mysql.business; 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.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 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.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; 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.business.vo.business.CrmBusinessPageReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO; 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.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils; import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -59,10 +60,36 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
return selectCount(CrmBusinessDO::getStatusTypeId, statusTypeId); 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>() return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
.eq(CrmBusinessDO::getCustomerId, customerId) .eq(CrmBusinessDO::getCustomerId, customerId)
.eq(CrmBusinessDO::getOwnerUserId, ownerUserId)); .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));
}
} }

View File

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

View File

@ -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);
}

View File

@ -194,4 +194,42 @@ public interface CrmBusinessService {
*/ */
List<CrmBusinessDO> getBusinessListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId); 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);
} }

View File

@ -375,4 +375,34 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
return businessMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId); 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);
}
} }

View File

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

View File

@ -47,6 +47,7 @@ import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*; 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 生效问题 * 获得自身的代理对象解决 AOP 生效问题
* *

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
/** /**
* CRM 客户画像 Service 实现类 * CRM 客户画像 Service 实现类
@ -55,15 +54,18 @@ public class CrmStatisticsPortraitServiceImpl implements CrmStatisticsPortraitSe
// 3. 拼接数据 // 3. 拼接数据
List<Area> areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area); 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); Map<Integer, Area> areaMap = convertMap(areaList, Area::getId);
return convertList(list, item -> { return convertList(list, item -> {
Integer parentId = AreaUtils.getParentIdByType(item.getAreaId(), AreaTypeEnum.PROVINCE); Integer parentId = AreaUtils.getParentIdByType(item.getAreaId(), AreaTypeEnum.PROVINCE);
if (parentId == null) { // 找不到归到未知 if (parentId != null) {
return item.setAreaId(null).setAreaName("未知"); 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("未知");
}); });
} }

View File

@ -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>