14 changed files with 1056 additions and 0 deletions
@ -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); |
||||
} |
||||
} |
||||
@ -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 整体商机转化统计(单行,字段同 OppTransformStatVo,source 为空) |
||||
*/ |
||||
OppTransformStatVo selectOppSummary(); |
||||
|
||||
/** |
||||
* 整体汇总统计"线索→商机→合同"成交数据(成交数、成交金额),不分来源。 |
||||
* 成交口径同 selectDealStatBySource(报价单 status='3')。 |
||||
* |
||||
* @return 整体成交统计(单行,字段同 DealTransformStatVo,source 为空) |
||||
*/ |
||||
DealTransformStatVo selectDealSummary(); |
||||
} |
||||
@ -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(); |
||||
} |
||||
@ -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; |
||||
} |
||||
@ -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; |
||||
} |
||||
@ -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(); |
||||
} |
||||
@ -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() + "%"; |
||||
} |
||||
} |
||||
@ -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; |
||||
} |
||||
@ -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; |
||||
} |
||||
@ -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; |
||||
} |
||||
@ -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; |
||||
} |
||||
@ -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; |
||||
} |
||||
@ -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> |
||||
@ -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…
Reference in new issue