Browse Source

线索转化分析

dev
myf 5 days ago
parent
commit
c7b963a9d0
  1. 51
      common/common-association/src/main/java/apelet/association/controller/ClueStatController.java
  2. 50
      common/common-association/src/main/java/apelet/association/dao/BusinessOpportunitiesMapper.java
  3. 27
      common/common-association/src/main/java/apelet/association/dao/MemberClueMapper.java
  4. 195
      common/common-association/src/main/java/apelet/association/model/BusinessOpportunities.java
  5. 158
      common/common-association/src/main/java/apelet/association/model/MemberClue.java
  6. 28
      common/common-association/src/main/java/apelet/association/service/ClueStatService.java
  7. 203
      common/common-association/src/main/java/apelet/association/service/impl/ClueStatServiceImpl.java
  8. 20
      common/common-association/src/main/java/apelet/association/vo/ClueSourceCountVo.java
  9. 56
      common/common-association/src/main/java/apelet/association/vo/ClueStatSummaryVo.java
  10. 82
      common/common-association/src/main/java/apelet/association/vo/ClueTransformStatVo.java
  11. 31
      common/common-association/src/main/java/apelet/association/vo/DealTransformStatVo.java
  12. 37
      common/common-association/src/main/java/apelet/association/vo/OppTransformStatVo.java
  13. 94
      common/common-association/src/main/resources/mapper/BusinessOpportunitiesMapper.xml
  14. 24
      common/common-association/src/main/resources/mapper/MemberClueMapper.xml

51
common/common-association/src/main/java/apelet/association/controller/ClueStatController.java

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
package apelet.association.controller;
import apelet.association.service.ClueStatService;
import apelet.association.vo.ClueStatSummaryVo;
import apelet.association.vo.ClueTransformStatVo;
import apelet.common.core.object.MyPageData;
import apelet.common.core.object.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 线索转化统计接口
*/
@RestController
@RequestMapping("/tenantadmin")
public class ClueStatController {
@Autowired
private ClueStatService clueStatService;
/**
* 查询每种线索来源的转化统计数据固定返回展会论坛/电话咨询/营销号/其他 4
* 支持分页 pageNum/pageSize 对统计结果做内存分页返回分页包装数据
*
* @param pageNum 页码 1 开始默认 1
* @param pageSize 每页条数默认 10
* @return 线索转化统计的分页数据
*/
@GetMapping("/clueTransformStat")
public ResponseResult<MyPageData<ClueTransformStatVo>> clueTransformStat(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
MyPageData<ClueTransformStatVo> result = clueStatService.getClueTransformStat(pageNum, pageSize);
return ResponseResult.success(result);
}
/**
* 查询线索转化统计整体汇总不分线索来源全量一行
* 指标转化商机金额赢单商机金额已转商机成交金额线索总数线索转化数线索转化率商机转化率
*
* @return 整体汇总统计结果
*/
@GetMapping("/clueStatSummary")
public ResponseResult<ClueStatSummaryVo> clueStatSummary() {
ClueStatSummaryVo result = clueStatService.getClueStatSummary();
return ResponseResult.success(result);
}
}

50
common/common-association/src/main/java/apelet/association/dao/BusinessOpportunitiesMapper.java

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
package apelet.association.dao;
import apelet.association.model.BusinessOpportunities;
import apelet.association.vo.DealTransformStatVo;
import apelet.association.vo.OppTransformStatVo;
import apelet.common.core.base.dao.BaseDaoMapper;
import java.util.List;
/**
* 商机表 Mapper
*/
public interface BusinessOpportunitiesMapper extends BaseDaoMapper<BusinessOpportunities> {
/**
* 按线索来源统计商机转化数据转化商机数赢单商机数赢单商机金额商机转化金额
* 商机通过 clue_id 关联线索表 member_clue再按线索来源 source 分组
*
* @return 线索来源维度的商机转化统计列表
*/
List<OppTransformStatVo> selectOppTransformStatBySource();
/**
* 按线索来源统计"线索→商机→合同"成交数据成交数成交金额
* <p>
* 当前合同表 contract 暂无数据报价单反写逻辑未做
* 暂以报价单 status = '3'通过即已成合同作为成交口径
* 报价单 quotation 通过 srcbillid 关联商机 business_opportunities
* 商机再通过 clue_id 关联线索 member_clue按线索来源 source 分组
*
* @return 线索来源维度的成交统计列表
*/
List<DealTransformStatVo> selectDealStatBySource();
/**
* 整体汇总统计商机转化数据转化商机数赢单商机数赢单商机金额商机转化金额不分来源
* 商机通过 clue_id 关联线索表 member_clue
*
* @return 整体商机转化统计单行字段同 OppTransformStatVosource 为空
*/
OppTransformStatVo selectOppSummary();
/**
* 整体汇总统计"线索→商机→合同"成交数据成交数成交金额不分来源
* 成交口径同 selectDealStatBySource报价单 status='3'
*
* @return 整体成交统计单行字段同 DealTransformStatVosource 为空
*/
DealTransformStatVo selectDealSummary();
}

27
common/common-association/src/main/java/apelet/association/dao/MemberClueMapper.java

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
package apelet.association.dao;
import apelet.association.model.MemberClue;
import apelet.association.vo.ClueSourceCountVo;
import apelet.common.core.base.dao.BaseDaoMapper;
import java.util.List;
/**
* 线索表 Mapper
*/
public interface MemberClueMapper extends BaseDaoMapper<MemberClue> {
/**
* 按线索来源统计每种来源的线索总数
*
* @return 线索来源维度的线索总数列表
*/
List<ClueSourceCountVo> selectClueTotalBySource();
/**
* 统计线索总数不分来源全量一行
*
* @return 线索总数
*/
Long selectClueTotalCount();
}

195
common/common-association/src/main/java/apelet/association/model/BusinessOpportunities.java

@ -0,0 +1,195 @@ @@ -0,0 +1,195 @@
package apelet.association.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 商机表 business_opportunities
*/
@Data
@TableName("business_opportunities")
public class BusinessOpportunities implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 创建人登录名称
*/
@TableField("create_user_id")
private String createUserId;
/**
* 创建日期
*/
@TableField("create_time")
private Date createTime;
/**
* 更新人登录名称
*/
@TableField("update_user_id")
private String updateUserId;
/**
* 更新日期
*/
@TableField("update_time")
private Date updateTime;
/**
* 逻辑删除标记(1: 正常 -1: 已删除)
*/
@TableField("deleted_flag")
private Integer deletedFlag;
/**
* 商机编号
*/
@TableField("number")
private String number;
/**
* 商机名称
*/
@TableField("name")
private String name;
/**
* 流程状态
*/
@TableField("flow_status")
private Integer flowStatus;
/**
* 流程最新状态
*/
@TableField("flow_approval_status")
private Integer flowApprovalStatus;
/**
* 流程实例id
*/
@TableField("pro_ins_id")
private String proInsId;
/**
* 单据状态
*/
@TableField("billstatus")
private String billstatus;
/**
* 公司
*/
@TableField("org")
private Integer org;
/**
* 来源单据ID
*/
@TableField("srcbillid")
private Integer srcbillid;
/**
* 来源单据编码
*/
@TableField("srcbillnumber")
private Integer srcbillnumber;
/**
* 来源分录Id
*/
@TableField("srcentryid")
private Integer srcentryid;
/**
* 单位ID
*/
@TableField("unit_id")
private String unitId;
/**
* 寻求类型
*/
@TableField("seek_type")
private String seekType;
/**
* 寻求信息
*/
@TableField("seek_info")
private String seekInfo;
/**
* 阶段表现(字典: 5表示赢单)
*/
@TableField("stage_manifestation")
private String stageManifestation;
/**
* 预计成交金额
*/
@TableField("predicted_price")
private BigDecimal predictedPrice;
/**
* 预计成交日期
*/
@TableField("predicted_signdate")
private Date predictedSigndate;
/**
* 下一步计划
*/
@TableField("next_plan")
private String nextPlan;
/**
* 商机类型
*/
@TableField("clue_type")
private String clueType;
/**
* 开始时间
*/
@TableField("start_time")
private Date startTime;
/**
* 结束时间
*/
@TableField("end_time")
private Date endTime;
/**
* 说明事项
*/
@TableField("explanation_matters")
private String explanationMatters;
/**
* 关联线索ID
*/
@TableField("clue_id")
private Long clueId;
/**
* 实际成交日期
*/
@TableField("actual_signdate")
private Date actualSigndate;
}

158
common/common-association/src/main/java/apelet/association/model/MemberClue.java

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
package apelet.association.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 线索表 member_clue
*/
@Data
@TableName("member_clue")
public class MemberClue implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 创建人登录名称
*/
@TableField("create_user_id")
private String createUserId;
/**
* 创建日期
*/
@TableField("create_time")
private Date createTime;
/**
* 更新人登录名称
*/
@TableField("update_user_id")
private String updateUserId;
/**
* 更新日期
*/
@TableField("update_time")
private Date updateTime;
/**
* 逻辑删除标记(1: 正常 -1: 已删除)
*/
@TableField("deleted_flag")
private Integer deletedFlag;
/**
* 单位ID
*/
@TableField("unit_id")
private Long unitId;
/**
* 寻求类型
*/
@TableField("seek_type")
private String seekType;
/**
* 寻求信息
*/
@TableField("seek_info")
private String seekInfo;
/**
* 下一步计划
*/
@TableField("next_plan")
private String nextPlan;
/**
* 线索编号
*/
@TableField("number")
private String number;
/**
* 线索名称
*/
@TableField("name")
private String name;
/**
* 流程状态
*/
@TableField("flow_status")
private Integer flowStatus;
/**
* 流程最新状态
*/
@TableField("flow_approval_status")
private Integer flowApprovalStatus;
/**
* 流程实例id
*/
@TableField("pro_ins_id")
private String proInsId;
/**
* 单据状态
*/
@TableField("billstatus")
private String billstatus;
/**
* 公司
*/
@TableField("org")
private Integer org;
/**
* 来源单据ID
*/
@TableField("srcbillid")
private Integer srcbillid;
/**
* 来源单据编码
*/
@TableField("srcbillnumber")
private Integer srcbillnumber;
/**
* 来源分录Id
*/
@TableField("srcentryid")
private Integer srcentryid;
/**
* 线索类型
*/
@TableField("clue_type")
private String clueType;
/**
* 线索来源(字典: 1展会论坛 2电话咨询 3营销号 4其他)
*/
@TableField("source")
private Integer source;
/**
* 状态
*/
@TableField("status")
private String status;
}

28
common/common-association/src/main/java/apelet/association/service/ClueStatService.java

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
package apelet.association.service;
import apelet.association.vo.ClueStatSummaryVo;
import apelet.association.vo.ClueTransformStatVo;
import apelet.common.core.object.MyPageData;
/**
* 线索转化统计 Service
*/
public interface ClueStatService {
/**
* 查询按线索来源分组的线索转化统计数据固定返回4种来源的行并做内存分页包装
*
* @param pageNum 页码 1 开始
* @param pageSize 每页条数
* @return 线索转化统计分页数据
*/
MyPageData<ClueTransformStatVo> getClueTransformStat(int pageNum, int pageSize);
/**
* 查询线索转化统计整体汇总数据不分线索来源全量一行
* 指标转化商机金额赢单商机金额已转商机成交金额线索总数线索转化数线索转化率商机转化率
*
* @return 整体汇总统计结果
*/
ClueStatSummaryVo getClueStatSummary();
}

203
common/common-association/src/main/java/apelet/association/service/impl/ClueStatServiceImpl.java

@ -0,0 +1,203 @@ @@ -0,0 +1,203 @@
package apelet.association.service.impl;
import apelet.association.dao.BusinessOpportunitiesMapper;
import apelet.association.dao.MemberClueMapper;
import apelet.association.service.ClueStatService;
import apelet.association.vo.ClueSourceCountVo;
import apelet.association.vo.ClueStatSummaryVo;
import apelet.association.vo.ClueTransformStatVo;
import apelet.association.vo.DealTransformStatVo;
import apelet.association.vo.OppTransformStatVo;
import apelet.common.core.object.MyPageData;
import apelet.common.core.util.MyPageUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 线索转化统计 Service 实现
*/
@Service
public class ClueStatServiceImpl implements ClueStatService {
/**
* 线索来源字典1展会论坛 2电话咨询 3营销号 4其他
* 与在线表单 zz_online_dict 中的"线索来源"字典保持一致
*/
private static final Map<Integer, String> SOURCE_MAP = new HashMap<>();
static {
SOURCE_MAP.put(1, "展会论坛");
SOURCE_MAP.put(2, "电话咨询");
SOURCE_MAP.put(3, "营销号");
SOURCE_MAP.put(4, "其他");
}
/**
* 赢单阶段值stage_manifestation = 5 表示赢单
*/
private static final String WIN_STAGE = "5";
@Autowired
private MemberClueMapper memberClueMapper;
@Autowired
private BusinessOpportunitiesMapper businessOpportunitiesMapper;
@Override
public MyPageData<ClueTransformStatVo> getClueTransformStat(int pageNum, int pageSize) {
// 0. 兜底处理非法分页参数(页码从1开始、每页至少1条)
int safePageNum = Math.max(pageNum, 1);
int safePageSize = Math.max(pageSize, 1);
// 1. 查询每种来源的线索总数
List<ClueSourceCountVo> clueTotalList = memberClueMapper.selectClueTotalBySource();
Map<Integer, Long> clueTotalMap = new HashMap<>();
for (ClueSourceCountVo item : clueTotalList) {
clueTotalMap.put(item.getSource(), item.getClueTotalCount());
}
// 2. 查询每种来源的商机转化统计(转化商机数、赢单商机数、赢单商机金额、商机转化金额)
List<OppTransformStatVo> oppStatList = businessOpportunitiesMapper.selectOppTransformStatBySource();
Map<Integer, OppTransformStatVo> oppStatMap = new HashMap<>();
for (OppTransformStatVo item : oppStatList) {
oppStatMap.put(item.getSource(), item);
}
// 3. 查询每种来源的成交统计(已转商机成交数、已转商机成交金额)
// 当前合同表 contract 暂无数据,暂以报价单 status='3'(通过,即已成合同)作为成交口径
List<DealTransformStatVo> dealStatList = businessOpportunitiesMapper.selectDealStatBySource();
Map<Integer, DealTransformStatVo> dealStatMap = new HashMap<>();
for (DealTransformStatVo item : dealStatList) {
dealStatMap.put(item.getSource(), item);
}
// 4. 固定返回4种来源的行,组装统计结果
List<ClueTransformStatVo> resultList = new ArrayList<>(SOURCE_MAP.size());
for (Map.Entry<Integer, String> entry : SOURCE_MAP.entrySet()) {
Integer source = entry.getKey();
String sourceName = entry.getValue();
Long clueTotalCount = clueTotalMap.getOrDefault(source, 0L);
OppTransformStatVo oppStat = oppStatMap.get(source);
long oppCount = oppStat == null ? 0L : oppStat.getOppCount();
long winOppCount = oppStat == null ? 0L : oppStat.getWinOppCount();
BigDecimal winOppAmount = oppStat == null ? BigDecimal.ZERO : oppStat.getWinOppAmount();
BigDecimal oppTransformAmount = oppStat == null ? BigDecimal.ZERO : oppStat.getOppTransformAmount();
DealTransformStatVo dealStat = dealStatMap.get(source);
long dealCount = dealStat == null ? 0L : dealStat.getDealCount();
BigDecimal dealAmount = dealStat == null ? BigDecimal.ZERO : dealStat.getDealAmount();
ClueTransformStatVo vo = new ClueTransformStatVo();
vo.setSource(source);
vo.setSourceName(sourceName);
// 线索转化数 = 转化商机数(该来源关联出的商机数量)
vo.setClueTransformCount(oppCount);
// 线索总数
vo.setClueTotalCount(clueTotalCount);
// 线索转化率 = 线索转化数 / 线索总数
vo.setClueTransformRate(calcRate(oppCount, clueTotalCount));
// 转化商机数 = 线索转化数
vo.setTransformOppCount(oppCount);
// 赢单商机数
vo.setWinOppCount(winOppCount);
// 赢单商机金额
vo.setWinOppAmount(winOppAmount);
// 商机转化金额 = 该来源所有商机的金额合计
vo.setOppTransformAmount(oppTransformAmount);
// 商机转化率 = 赢单商机数 / 转化商机数
vo.setOppTransformRate(calcRate(winOppCount, oppCount));
// 已转商机成交数 = 线索转商机转合同的数量
vo.setDealCount(dealCount);
// 已转商机成交金额 = 线索转商机转合同的金额合计
vo.setDealAmount(dealAmount);
// 已转商机成交率 = 已转商机成交数 / 赢单商机数
vo.setDealRate(calcRate(dealCount, winOppCount));
resultList.add(vo);
}
// 5. 对统计结果做内存分页并包装返回(总行数为固定来源数,最多 4 行)
return paginate(resultList, safePageNum, safePageSize);
}
/**
* 对统计结果列表做内存分页并包装为 {@link MyPageData}
* 分页在内存中完成该接口固定返回少量汇总行 SQL 分页的必要
*
* @param list 全量统计结果
* @param pageNum 页码 1 开始
* @param pageSize 每页条数
* @return 分页包装数据
*/
private MyPageData<ClueTransformStatVo> paginate(List<ClueTransformStatVo> list, int pageNum, int pageSize) {
int total = list.size();
int fromIndex = (pageNum - 1) * pageSize;
// 起始位置超出总行数时返回空列表,但保留总数供前端分页组件使用
if (fromIndex >= total) {
return MyPageUtil.makeResponseData(new ArrayList<>(), (long) total);
}
int toIndex = Math.min(fromIndex + pageSize, total);
return MyPageUtil.makeResponseData(new ArrayList<>(list.subList(fromIndex, toIndex)), (long) total);
}
@Override
public ClueStatSummaryVo getClueStatSummary() {
// 1. 线索总数(全量)
Long clueTotalCount = memberClueMapper.selectClueTotalCount();
long clueTotal = clueTotalCount == null ? 0L : clueTotalCount;
// 2. 商机转化整体统计(转化商机数、赢单商机数、赢单商机金额、商机转化金额)
OppTransformStatVo oppStat = businessOpportunitiesMapper.selectOppSummary();
long oppCount = oppStat == null ? 0L : oppStat.getOppCount();
long winOppCount = oppStat == null ? 0L : oppStat.getWinOppCount();
BigDecimal winOppAmount = oppStat == null ? BigDecimal.ZERO : oppStat.getWinOppAmount();
BigDecimal oppTransformAmount = oppStat == null ? BigDecimal.ZERO : oppStat.getOppTransformAmount();
// 3. 成交整体统计(已转商机成交金额)
DealTransformStatVo dealStat = businessOpportunitiesMapper.selectDealSummary();
BigDecimal dealAmount = dealStat == null ? BigDecimal.ZERO : dealStat.getDealAmount();
ClueStatSummaryVo vo = new ClueStatSummaryVo();
// 转化商机金额(所有关联到线索的商机金额合计)
vo.setOppTransformAmount(oppTransformAmount);
// 赢单商机金额
vo.setWinOppAmount(winOppAmount);
// 已转商机成交金额(线索转商机转合同的金额合计)
vo.setDealAmount(dealAmount);
// 线索总数
vo.setClueTotalCount(clueTotal);
// 线索转化数 = 转化商机数(关联到线索的商机数量)
vo.setClueTransformCount(oppCount);
// 线索转化率 = 线索转化数 / 线索总数
vo.setClueTransformRate(calcRate(oppCount, clueTotal));
// 商机转化率 = 赢单商机数 / 转化商机数
vo.setOppTransformRate(calcRate(winOppCount, oppCount));
return vo;
}
/**
* 计算转化率返回保留两位小数的百分比字符串 66.67%
* 分子或分母为空或分母为 0 时返回 null表示无法计算
*
* @param numerator 分子
* @param denominator 分母
* @return 百分比字符串无法计算时返回 null
*/
private String calcRate(long numerator, long denominator) {
if (denominator == 0L) {
return null;
}
BigDecimal rate = BigDecimal.valueOf(numerator)
.multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(denominator), 2, RoundingMode.HALF_UP);
return rate.setScale(2).stripTrailingZeros().toPlainString() + "%";
}
}

20
common/common-association/src/main/java/apelet/association/vo/ClueSourceCountVo.java

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
package apelet.association.vo;
import lombok.Data;
/**
* 线索来源对应的线索总数统计结果中间结果
*/
@Data
public class ClueSourceCountVo {
/**
* 线索来源(字典: 1展会论坛 2电话咨询 3营销号 4其他)
*/
private Integer source;
/**
* 该来源的线索总数
*/
private Long clueTotalCount;
}

56
common/common-association/src/main/java/apelet/association/vo/ClueStatSummaryVo.java

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
package apelet.association.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 线索转化统计整体汇总结果对象不分线索来源全量一行
* <p>
* 指标口径
* - 转化商机金额所有关联到线索的商机 predicted_price 合计
* - 赢单商机金额stage_manifestation=5 的商机 predicted_price 合计
* - 已转商机成交金额报价单 status='3'通过即已成合同 qty 合计
* - 线索总数member_clue 总条数
* - 线索转化数关联到线索的商机数线索商机
* - 线索转化率线索转化数 / 线索总数
* - 商机转化率赢单商机数 / 转化商机数
*/
@Data
public class ClueStatSummaryVo {
/**
* 转化商机金额所有关联到线索的商机 predicted_price 合计
*/
private BigDecimal oppTransformAmount;
/**
* 赢单商机金额stage_manifestation=5 predicted_price 合计
*/
private BigDecimal winOppAmount;
/**
* 已转商机成交金额线索商机合同的金额合计当前以报价单 status='3' 作为成交口径
*/
private BigDecimal dealAmount;
/**
* 线索总数
*/
private Long clueTotalCount;
/**
* 线索转化数关联到线索的商机数
*/
private Long clueTransformCount;
/**
* 线索转化率线索转化数 / 线索总数百分比字符串
*/
private String clueTransformRate;
/**
* 商机转化率赢单商机数 / 转化商机数百分比字符串
*/
private String oppTransformRate;
}

82
common/common-association/src/main/java/apelet/association/vo/ClueTransformStatVo.java

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
package apelet.association.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 线索转化统计结果对象按线索来源维度展示每种线索的转化数据
* <p>
* 对应表头
* 1 线索来源 / 2 线索转化数 / 3 线索总数 / 4 线索转化率 / 5 转化商机数 /
* 6 赢单商机数 / 7 赢单商机金额 / 8 商机转化金额 / 9 商机转化率 /
* 10 已转商机成交数 / 11 已转商机成交金额 / 12 已转商机成交率
*/
@Data
public class ClueTransformStatVo {
/**
* 线索来源key(字典: 1展会论坛 2电话咨询 3营销号 4其他)
*/
private Integer source;
/**
* 线索来源中文名
*/
private String sourceName;
/**
* 线索转化数该来源关联出的商机数量
*/
private Long clueTransformCount;
/**
* 线索总数该来源的线索数量
*/
private Long clueTotalCount;
/**
* 线索转化率线索转化数 / 线索总数百分比字符串
*/
private String clueTransformRate;
/**
* 转化商机数= 线索转化数
*/
private Long transformOppCount;
/**
* 赢单商机数stage_manifestation=5 的商机数量
*/
private Long winOppCount;
/**
* 赢单商机金额stage_manifestation=5 predicted_price 合计
*/
private BigDecimal winOppAmount;
/**
* 商机转化金额该来源线索关联出的所有商机 predicted_price 合计与是否赢单无关
*/
private BigDecimal oppTransformAmount;
/**
* 商机转化率赢单商机数 / 转化商机数百分比字符串
*/
private String oppTransformRate;
/**
* 已转商机成交数线索商机合同的数量当前以报价单 status='3' 作为成交口径
*/
private Long dealCount;
/**
* 已转商机成交金额线索商机合同的金额合计报价单 status='3' qty 求和
*/
private BigDecimal dealAmount;
/**
* 已转商机成交率已转商机成交数 / 赢单商机数百分比字符串
*/
private String dealRate;
}

31
common/common-association/src/main/java/apelet/association/vo/DealTransformStatVo.java

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
package apelet.association.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 按线索来源维度统计的"线索→商机→合同"成交中间结果
* <p>
* 说明合同表 contract 暂未接入数据报价单反写逻辑未做
* 当前以报价单 status = '3'通过即已成合同作为成交口径
* 报价单通过 srcbillid 关联商机 business_opportunities
*/
@Data
public class DealTransformStatVo {
/**
* 线索来源(字典: 1展会论坛 2电话咨询 3营销号 4其他)
*/
private Integer source;
/**
* 该来源已转商机成交数报价单 status='3' 的数量即线索商机合同的数量
*/
private Long dealCount;
/**
* 该来源已转商机成交金额合计报价单 status='3' qty 求和即合同金额合计
*/
private BigDecimal dealAmount;
}

37
common/common-association/src/main/java/apelet/association/vo/OppTransformStatVo.java

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
package apelet.association.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 按线索来源维度统计的商机转化中间结果
*/
@Data
public class OppTransformStatVo {
/**
* 线索来源(字典: 1展会论坛 2电话咨询 3营销号 4其他)
*/
private Integer source;
/**
* 该来源转化的商机数商机关联线索后按来源分组
*/
private Long oppCount;
/**
* 该来源赢单商机数stage_manifestation=5
*/
private Long winOppCount;
/**
* 该来源赢单商机金额合计stage_manifestation=5 predicted_price 求和
*/
private BigDecimal winOppAmount;
/**
* 该来源商机转化金额合计该来源线索关联出的所有商机 predicted_price 求和与是否赢单无关
*/
private BigDecimal oppTransformAmount;
}

94
common/common-association/src/main/resources/mapper/BusinessOpportunitiesMapper.xml

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
<?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="apelet.association.dao.BusinessOpportunitiesMapper">
<!--
按线索来源统计商机转化数据:
- oppCount 转化商机数(该来源关联出的商机总数)
- winOppCount 赢单商机数(stage_manifestation = '5')
- winOppAmount 赢单商机金额(stage_manifestation = '5' 的 predicted_price 合计)
- oppTransformAmount 商机转化金额(该来源关联出的所有商机 predicted_price 合计,与是否赢单无关)
商机通过 clue_id 关联线索表 member_clue,按线索来源 source 分组统计。
-->
<select id="selectOppTransformStatBySource"
resultType="apelet.association.vo.OppTransformStatVo">
SELECT
mc.source AS source,
COUNT(bo.id) AS oppCount,
SUM(CASE WHEN bo.stage_manifestation = '5' THEN 1 ELSE 0 END) AS winOppCount,
COALESCE(SUM(CASE WHEN bo.stage_manifestation = '5' THEN bo.predicted_price ELSE 0 END), 0) AS winOppAmount,
COALESCE(SUM(bo.predicted_price), 0) AS oppTransformAmount
FROM business_opportunities bo
INNER JOIN member_clue mc ON bo.clue_id = mc.id
WHERE bo.deleted_flag = 1
AND mc.deleted_flag = 1
GROUP BY mc.source
</select>
<!--
按线索来源统计"线索→商机→合同"成交数据:
- dealCount 已转商机成交数(报价单 status = '3' 的数量)
- dealAmount 已转商机成交金额(报价单 status = '3' 的 qty 合计)
说明:合同表 contract 暂无数据(报价单反写逻辑未做),
暂以报价单 status = '3'(通过,即已成合同)作为成交口径。
报价单 quotation 通过 srcbillid 关联商机 business_opportunities,
商机再通过 clue_id 关联线索表 member_clue,按线索来源 source 分组统计。
-->
<select id="selectDealStatBySource"
resultType="apelet.association.vo.DealTransformStatVo">
SELECT
mc.source AS source,
COUNT(DISTINCT q.id) AS dealCount,
COALESCE(SUM(q.qty), 0) AS dealAmount
FROM quotation q
INNER JOIN business_opportunities bo ON q.srcbillid = bo.id
INNER JOIN member_clue mc ON bo.clue_id = mc.id
WHERE q.status = '3'
AND q.deleted_flag = 1
AND bo.deleted_flag = 1
AND mc.deleted_flag = 1
GROUP BY mc.source
</select>
<!--
整体汇总统计商机转化数据(不分线索来源,返回单行):
- oppCount 转化商机数(关联到线索的商机总数)
- winOppCount 赢单商机数(stage_manifestation = '5')
- winOppAmount 赢单商机金额(stage_manifestation = '5' 的 predicted_price 合计)
- oppTransformAmount 商机转化金额(所有商机 predicted_price 合计,与是否赢单无关)
-->
<select id="selectOppSummary"
resultType="apelet.association.vo.OppTransformStatVo">
SELECT
COUNT(bo.id) AS oppCount,
COALESCE(SUM(CASE WHEN bo.stage_manifestation = '5' THEN 1 ELSE 0 END), 0) AS winOppCount,
COALESCE(SUM(CASE WHEN bo.stage_manifestation = '5' THEN bo.predicted_price ELSE 0 END), 0) AS winOppAmount,
COALESCE(SUM(bo.predicted_price), 0) AS oppTransformAmount
FROM business_opportunities bo
INNER JOIN member_clue mc ON bo.clue_id = mc.id
WHERE bo.deleted_flag = 1
AND mc.deleted_flag = 1
</select>
<!--
整体汇总统计"线索→商机→合同"成交数据(不分线索来源,返回单行):
- dealCount 已转商机成交数(报价单 status = '3' 的数量)
- dealAmount 已转商机成交金额(报价单 status = '3' 的 qty 合计)
成交口径同 selectDealStatBySource(合同表 contract 暂无数据,暂以报价单 status='3' 作为成交口径)。
-->
<select id="selectDealSummary"
resultType="apelet.association.vo.DealTransformStatVo">
SELECT
COUNT(DISTINCT q.id) AS dealCount,
COALESCE(SUM(q.qty), 0) AS dealAmount
FROM quotation q
INNER JOIN business_opportunities bo ON q.srcbillid = bo.id
INNER JOIN member_clue mc ON bo.clue_id = mc.id
WHERE q.status = '3'
AND q.deleted_flag = 1
AND bo.deleted_flag = 1
AND mc.deleted_flag = 1
</select>
</mapper>

24
common/common-association/src/main/resources/mapper/MemberClueMapper.xml

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
<?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="apelet.association.dao.MemberClueMapper">
<!-- 按线索来源统计每种来源的线索总数 -->
<select id="selectClueTotalBySource"
resultType="apelet.association.vo.ClueSourceCountVo">
SELECT
source AS source,
COUNT(*) AS clueTotalCount
FROM member_clue
WHERE deleted_flag = 1
GROUP BY source
</select>
<!-- 统计线索总数(不分来源,全量一行) -->
<select id="selectClueTotalCount" resultType="java.lang.Long">
SELECT COUNT(*)
FROM member_clue
WHERE deleted_flag = 1
</select>
</mapper>
Loading…
Cancel
Save