|
@@ -0,0 +1,273 @@
|
|
|
|
|
+package org.dromara.customer.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import org.dromara.customer.domain.*;
|
|
|
|
|
+import org.dromara.customer.domain.vo.WorkbenchStatVo;
|
|
|
|
|
+import org.dromara.customer.mapper.*;
|
|
|
|
|
+import org.dromara.customer.service.IWorkbenchService;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 工作台Service业务层处理
|
|
|
|
|
+ */
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+@Service
|
|
|
|
|
+public class WorkbenchServiceImpl implements IWorkbenchService {
|
|
|
|
|
+
|
|
|
|
|
+ private final SalesleadsMapper salesleadsMapper;
|
|
|
|
|
+ private final SalesAnnualFinalizationMapper salesAnnualFinalizationMapper;
|
|
|
|
|
+ private final FollowUpLogMapper followUpLogMapper;
|
|
|
|
|
+ private final CustomerInfoMapper customerInfoMapper;
|
|
|
|
|
+ private final CustomerSalesInfoMapper customerSalesInfoMapper;
|
|
|
|
|
+ private final SalesresultanalyzeMapper salesresultanalyzeMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public WorkbenchStatVo getStat() {
|
|
|
|
|
+ WorkbenchStatVo vo = new WorkbenchStatVo();
|
|
|
|
|
+ vo.setOpportunityStats(getOpportunityStats());
|
|
|
|
|
+ vo.setSelectionStats(getSelectionStats());
|
|
|
|
|
+ vo.setOpportunityFunnel(getOpportunityFunnel());
|
|
|
|
|
+ vo.setCustomerFunnel(getCustomerFunnel());
|
|
|
|
|
+ vo.setVisitStats(getVisitStats());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<WorkbenchStatVo.StatItem> getOpportunityFunnel() {
|
|
|
|
|
+ // 1. 销售线索:iz_clue = 1
|
|
|
|
|
+ Long clueCount = salesleadsMapper.selectCount(new LambdaQueryWrapper<Salesleads>()
|
|
|
|
|
+ .eq(Salesleads::getIzClue, 1)
|
|
|
|
|
+ .eq(Salesleads::getDelFlag, "0"));
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 商机:iz_clue = 0
|
|
|
|
|
+ Long opportunityCount = salesleadsMapper.selectCount(new LambdaQueryWrapper<Salesleads>()
|
|
|
|
|
+ .eq(Salesleads::getIzClue, 0)
|
|
|
|
|
+ .eq(Salesleads::getDelFlag, "0"));
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 赢单:salesresultanalyze 中 dealResult = 1, dataType = 2 (商机)
|
|
|
|
|
+ Long winCount = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
|
|
|
|
|
+ .eq(SalesResultAnalyze::getDealResult, 1)
|
|
|
|
|
+ .eq(SalesResultAnalyze::getDataType, 2)
|
|
|
|
|
+ .eq(SalesResultAnalyze::getIsDelete, 0));
|
|
|
|
|
+
|
|
|
|
|
+ List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("销售线索", String.valueOf(clueCount), "个"));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("商机", String.valueOf(opportunityCount), "个"));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("赢单", String.valueOf(winCount), "个"));
|
|
|
|
|
+ return stats;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<WorkbenchStatVo.StatItem> getCustomerFunnel() {
|
|
|
|
|
+ // 1. 正式客户:在 sales_info 表中 sales_person_id 和 service_staff_id 都不为空
|
|
|
|
|
+ Long officialCount = customerSalesInfoMapper.selectCount(new LambdaQueryWrapper<CustomerSalesInfo>()
|
|
|
|
|
+ .isNotNull(CustomerSalesInfo::getSalesPersonId)
|
|
|
|
|
+ .isNotNull(CustomerSalesInfo::getServiceStaffId)
|
|
|
|
|
+ .eq(CustomerSalesInfo::getDelFlag, "0"));
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 公海客户:总客户数 - 正式客户数 (或者只要有一个为空即为公海)
|
|
|
|
|
+ Long totalCount = customerInfoMapper.selectCount(new LambdaQueryWrapper<CustomerInfo>()
|
|
|
|
|
+ .eq(CustomerInfo::getDelFlag, "0"));
|
|
|
|
|
+
|
|
|
|
|
+ Long publicCount = Math.max(0L, totalCount - officialCount);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 已成交:这里采用 Salesresultanalyze 中已成交的记录数作为“已成交”阶段的量度
|
|
|
|
|
+ Long dealCount = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
|
|
|
|
|
+ .eq(SalesResultAnalyze::getDealResult, 1)
|
|
|
|
|
+ .eq(SalesResultAnalyze::getIsDelete, 0));
|
|
|
|
|
+
|
|
|
|
|
+ List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("公海客户数", String.valueOf(publicCount), "个"));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("正式客户", String.valueOf(officialCount), "个"));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("已成交", String.valueOf(dealCount), "个"));
|
|
|
|
|
+ return stats;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<WorkbenchStatVo.StatItem> getVisitStats() {
|
|
|
|
|
+ // 统计本月数据
|
|
|
|
|
+ Calendar calendar = Calendar.getInstance();
|
|
|
|
|
+ calendar.set(Calendar.DAY_OF_MONTH, 1);
|
|
|
|
|
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
|
|
|
|
|
+ calendar.set(Calendar.MINUTE, 0);
|
|
|
|
|
+ calendar.set(Calendar.SECOND, 0);
|
|
|
|
|
+ Date monthStart = calendar.getTime();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 拜访客户数:从 followuplog 中统计本月去重的客户
|
|
|
|
|
+ List<FollowUpLog> visitRecords = followUpLogMapper.selectList(new LambdaQueryWrapper<FollowUpLog>()
|
|
|
|
|
+ .ge(FollowUpLog::getCallDate, monthStart)
|
|
|
|
|
+ .eq(FollowUpLog::getIsDelete, 0));
|
|
|
|
|
+ long visitCustomerCount = visitRecords.stream()
|
|
|
|
|
+ .map(FollowUpLog::getCustomerName)
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .distinct()
|
|
|
|
|
+ .count();
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 拜访次数
|
|
|
|
|
+ int visitCount = visitRecords.size();
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 销售金额:赢单项目的预算总和
|
|
|
|
|
+ List<SalesResultAnalyze> winList = salesresultanalyzeMapper.selectList(new LambdaQueryWrapper<SalesResultAnalyze>()
|
|
|
|
|
+ .eq(SalesResultAnalyze::getDealResult, 1)
|
|
|
|
|
+ .eq(SalesResultAnalyze::getIsDelete, 0));
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal totalAmount = BigDecimal.ZERO;
|
|
|
|
|
+ if (!winList.isEmpty()) {
|
|
|
|
|
+ List<String> winNos = winList.stream().map(SalesResultAnalyze::getObjectNo).collect(Collectors.toList());
|
|
|
|
|
+ List<Salesleads> wins = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>()
|
|
|
|
|
+ .in(Salesleads::getProjectNo, winNos)
|
|
|
|
|
+ .eq(Salesleads::getDelFlag, "0"));
|
|
|
|
|
+ totalAmount = wins.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("拜访客户数", String.valueOf(visitCustomerCount), ""));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("拜访次数", String.valueOf(visitCount), ""));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("销售金额", totalAmount.setScale(0, BigDecimal.ROUND_HALF_UP).toString(), ""));
|
|
|
|
|
+ return stats;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<WorkbenchStatVo.StatItem> getOpportunityStats() {
|
|
|
|
|
+ // 1. 查询所有商机 (izClue = 0)
|
|
|
|
|
+ List<Salesleads> list = salesleadsMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<Salesleads>()
|
|
|
|
|
+ .eq(Salesleads::getIzClue, 0)
|
|
|
|
|
+ .eq(Salesleads::getDelFlag, "0")
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 获取所有项目编号
|
|
|
|
|
+ List<String> projectNos = list.stream().map(Salesleads::getProjectNo).filter(Objects::nonNull).collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 查询跟进记录
|
|
|
|
|
+ Map<String, Date> lastFollowUpMap = new HashMap<>();
|
|
|
|
|
+ if (!projectNos.isEmpty()) {
|
|
|
|
|
+ List<FollowUpLog> logs = followUpLogMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<FollowUpLog>()
|
|
|
|
|
+ .in(FollowUpLog::getObjectNo, projectNos)
|
|
|
|
|
+ .orderByDesc(FollowUpLog::getCallDate)
|
|
|
|
|
+ );
|
|
|
|
|
+ // 每个项目保留最后一次跟进时间
|
|
|
|
|
+ for (FollowUpLog log : logs) {
|
|
|
|
|
+ lastFollowUpMap.putIfAbsent(log.getObjectNo(), log.getCallDate());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 统计
|
|
|
|
|
+ int count = list.size();
|
|
|
|
|
+ BigDecimal amount = list.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
|
+ int unFollow3 = 0;
|
|
|
|
|
+ int unFollow7 = 0;
|
|
|
|
|
+ int unFollowOver7 = 0;
|
|
|
|
|
+ int retention30 = 0;
|
|
|
|
|
+
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ long nowMs = now.getTime();
|
|
|
|
|
+ long dayMs = 24 * 60 * 60 * 1000L;
|
|
|
|
|
+
|
|
|
|
|
+ for (Salesleads item : list) {
|
|
|
|
|
+ Date lastDate = lastFollowUpMap.get(item.getProjectNo());
|
|
|
|
|
+ if (lastDate == null) {
|
|
|
|
|
+ lastDate = item.getCreateTime(); // 如果没有跟进记录,则以创建时间为准
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (lastDate != null) {
|
|
|
|
|
+ long diffDays = (nowMs - lastDate.getTime()) / dayMs;
|
|
|
|
|
+ if (diffDays >= 3 && diffDays < 7) {
|
|
|
|
|
+ unFollow3++;
|
|
|
|
|
+ } else if (diffDays == 7) {
|
|
|
|
|
+ unFollow7++;
|
|
|
|
|
+ } else if (diffDays > 7) {
|
|
|
|
|
+ unFollowOver7++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (item.getCreateTime() != null) {
|
|
|
|
|
+ long stayDays = (nowMs - item.getCreateTime().getTime()) / dayMs;
|
|
|
|
|
+ if (stayDays > 30) {
|
|
|
|
|
+ retention30++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("商机数", String.valueOf(count)));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("金额(万)", amount.setScale(2, BigDecimal.ROUND_HALF_UP).toString()));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("3天未跟进", String.valueOf(unFollow3)));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("7天未跟进", String.valueOf(unFollow7)));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("7天以上未跟进", String.valueOf(unFollowOver7)));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("滞留超过30天", String.valueOf(retention30)));
|
|
|
|
|
+ return stats;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<WorkbenchStatVo.StatItem> getSelectionStats() {
|
|
|
|
|
+ // 1. 查询所有年度入围
|
|
|
|
|
+ List<SalesAnnualFinalization> list = salesAnnualFinalizationMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<SalesAnnualFinalization>()
|
|
|
|
|
+ .eq(SalesAnnualFinalization::getIsDelete, 0)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 获取所有项目编号
|
|
|
|
|
+ List<String> projectNos = list.stream().map(SalesAnnualFinalization::getProjectNo).filter(Objects::nonNull).collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 查询跟进记录
|
|
|
|
|
+ Map<String, Date> lastFollowUpMap = new HashMap<>();
|
|
|
|
|
+ if (!projectNos.isEmpty()) {
|
|
|
|
|
+ List<FollowUpLog> logs = followUpLogMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<FollowUpLog>()
|
|
|
|
|
+ .in(FollowUpLog::getObjectNo, projectNos)
|
|
|
|
|
+ .orderByDesc(FollowUpLog::getCallDate)
|
|
|
|
|
+ );
|
|
|
|
|
+ for (FollowUpLog log : logs) {
|
|
|
|
|
+ lastFollowUpMap.putIfAbsent(log.getObjectNo(), log.getCallDate());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 统计
|
|
|
|
|
+ int count = list.size();
|
|
|
|
|
+ BigDecimal amount = list.stream().map(SalesAnnualFinalization::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
|
+ int unFollow3 = 0;
|
|
|
|
|
+ int unFollowOver3 = 0;
|
|
|
|
|
+ int unFollowOver7 = 0;
|
|
|
|
|
+ int retention30 = 0;
|
|
|
|
|
+
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ long nowMs = now.getTime();
|
|
|
|
|
+ long dayMs = 24 * 60 * 60 * 1000L;
|
|
|
|
|
+
|
|
|
|
|
+ for (SalesAnnualFinalization item : list) {
|
|
|
|
|
+ Date lastDate = lastFollowUpMap.get(item.getProjectNo());
|
|
|
|
|
+ if (lastDate == null) {
|
|
|
|
|
+ lastDate = item.getCreateTime();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (lastDate != null) {
|
|
|
|
|
+ long diffDays = (nowMs - lastDate.getTime()) / dayMs;
|
|
|
|
|
+ if (diffDays >= 3 && diffDays < 7) {
|
|
|
|
|
+ unFollow3++;
|
|
|
|
|
+ unFollowOver3++;
|
|
|
|
|
+ } else if (diffDays >= 7) {
|
|
|
|
|
+ unFollowOver3++;
|
|
|
|
|
+ unFollowOver7++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (item.getCreateTime() != null) {
|
|
|
|
|
+ long stayDays = (nowMs - item.getCreateTime().getTime()) / dayMs;
|
|
|
|
|
+ if (stayDays > 30) {
|
|
|
|
|
+ retention30++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("入围项目数", String.valueOf(count)));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("金额(万)", amount.setScale(2, BigDecimal.ROUND_HALF_UP).toString()));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("3天未跟进", String.valueOf(unFollow3)));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("3天以上未跟进", String.valueOf(unFollowOver3)));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("7天以上未跟进", String.valueOf(unFollowOver7)));
|
|
|
|
|
+ stats.add(new WorkbenchStatVo.StatItem("滞留超过30天", String.valueOf(retention30)));
|
|
|
|
|
+ return stats;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|