From c7b963a9d057167e40c4814421ee78636921e6c7 Mon Sep 17 00:00:00 2001 From: myf Date: Mon, 24 Aug 2026 14:39:42 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=BF=E7=B4=A2=E8=BD=AC=E5=8C=96=E5=88=86?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../association/controller/ClueStatController.java | 51 ++++++ .../dao/BusinessOpportunitiesMapper.java | 50 +++++ .../apelet/association/dao/MemberClueMapper.java | 27 +++ .../association/model/BusinessOpportunities.java | 195 ++++++++++++++++++++ .../java/apelet/association/model/MemberClue.java | 158 ++++++++++++++++ .../association/service/ClueStatService.java | 28 +++ .../service/impl/ClueStatServiceImpl.java | 203 +++++++++++++++++++++ .../apelet/association/vo/ClueSourceCountVo.java | 20 ++ .../apelet/association/vo/ClueStatSummaryVo.java | 56 ++++++ .../apelet/association/vo/ClueTransformStatVo.java | 82 +++++++++ .../apelet/association/vo/DealTransformStatVo.java | 31 ++++ .../apelet/association/vo/OppTransformStatVo.java | 37 ++++ .../mapper/BusinessOpportunitiesMapper.xml | 94 ++++++++++ .../src/main/resources/mapper/MemberClueMapper.xml | 24 +++ 14 files changed, 1056 insertions(+) create mode 100644 common/common-association/src/main/java/apelet/association/controller/ClueStatController.java create mode 100644 common/common-association/src/main/java/apelet/association/dao/BusinessOpportunitiesMapper.java create mode 100644 common/common-association/src/main/java/apelet/association/dao/MemberClueMapper.java create mode 100644 common/common-association/src/main/java/apelet/association/model/BusinessOpportunities.java create mode 100644 common/common-association/src/main/java/apelet/association/model/MemberClue.java create mode 100644 common/common-association/src/main/java/apelet/association/service/ClueStatService.java create mode 100644 common/common-association/src/main/java/apelet/association/service/impl/ClueStatServiceImpl.java create mode 100644 common/common-association/src/main/java/apelet/association/vo/ClueSourceCountVo.java create mode 100644 common/common-association/src/main/java/apelet/association/vo/ClueStatSummaryVo.java create mode 100644 common/common-association/src/main/java/apelet/association/vo/ClueTransformStatVo.java create mode 100644 common/common-association/src/main/java/apelet/association/vo/DealTransformStatVo.java create mode 100644 common/common-association/src/main/java/apelet/association/vo/OppTransformStatVo.java create mode 100644 common/common-association/src/main/resources/mapper/BusinessOpportunitiesMapper.xml create mode 100644 common/common-association/src/main/resources/mapper/MemberClueMapper.xml diff --git a/common/common-association/src/main/java/apelet/association/controller/ClueStatController.java b/common/common-association/src/main/java/apelet/association/controller/ClueStatController.java new file mode 100644 index 0000000..bbb5429 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/controller/ClueStatController.java @@ -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> clueTransformStat( + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + MyPageData result = clueStatService.getClueTransformStat(pageNum, pageSize); + return ResponseResult.success(result); + } + + /** + * 查询线索转化统计整体汇总(不分线索来源,全量一行)。 + * 指标:转化商机金额、赢单商机金额、已转商机成交金额、线索总数、线索转化数、线索转化率、商机转化率。 + * + * @return 整体汇总统计结果 + */ + @GetMapping("/clueStatSummary") + public ResponseResult clueStatSummary() { + ClueStatSummaryVo result = clueStatService.getClueStatSummary(); + return ResponseResult.success(result); + } +} diff --git a/common/common-association/src/main/java/apelet/association/dao/BusinessOpportunitiesMapper.java b/common/common-association/src/main/java/apelet/association/dao/BusinessOpportunitiesMapper.java new file mode 100644 index 0000000..5b6d3bf --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/dao/BusinessOpportunitiesMapper.java @@ -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 { + + /** + * 按线索来源统计商机转化数据(转化商机数、赢单商机数、赢单商机金额、商机转化金额)。 + * 商机通过 clue_id 关联线索表 member_clue,再按线索来源 source 分组。 + * + * @return 线索来源维度的商机转化统计列表 + */ + List selectOppTransformStatBySource(); + + /** + * 按线索来源统计"线索→商机→合同"成交数据(成交数、成交金额)。 + *

+ * 当前合同表 contract 暂无数据(报价单反写逻辑未做), + * 暂以报价单 status = '3'(通过,即已成合同)作为成交口径: + * 报价单 quotation 通过 srcbillid 关联商机 business_opportunities, + * 商机再通过 clue_id 关联线索 member_clue,按线索来源 source 分组。 + * + * @return 线索来源维度的成交统计列表 + */ + List selectDealStatBySource(); + + /** + * 整体汇总统计商机转化数据(转化商机数、赢单商机数、赢单商机金额、商机转化金额),不分来源。 + * 商机通过 clue_id 关联线索表 member_clue。 + * + * @return 整体商机转化统计(单行,字段同 OppTransformStatVo,source 为空) + */ + OppTransformStatVo selectOppSummary(); + + /** + * 整体汇总统计"线索→商机→合同"成交数据(成交数、成交金额),不分来源。 + * 成交口径同 selectDealStatBySource(报价单 status='3')。 + * + * @return 整体成交统计(单行,字段同 DealTransformStatVo,source 为空) + */ + DealTransformStatVo selectDealSummary(); +} diff --git a/common/common-association/src/main/java/apelet/association/dao/MemberClueMapper.java b/common/common-association/src/main/java/apelet/association/dao/MemberClueMapper.java new file mode 100644 index 0000000..fa865f4 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/dao/MemberClueMapper.java @@ -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 { + + /** + * 按线索来源统计每种来源的线索总数。 + * + * @return 线索来源维度的线索总数列表 + */ + List selectClueTotalBySource(); + + /** + * 统计线索总数(不分来源,全量一行)。 + * + * @return 线索总数 + */ + Long selectClueTotalCount(); +} diff --git a/common/common-association/src/main/java/apelet/association/model/BusinessOpportunities.java b/common/common-association/src/main/java/apelet/association/model/BusinessOpportunities.java new file mode 100644 index 0000000..69bede5 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/model/BusinessOpportunities.java @@ -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; +} diff --git a/common/common-association/src/main/java/apelet/association/model/MemberClue.java b/common/common-association/src/main/java/apelet/association/model/MemberClue.java new file mode 100644 index 0000000..1eac12d --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/model/MemberClue.java @@ -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; +} diff --git a/common/common-association/src/main/java/apelet/association/service/ClueStatService.java b/common/common-association/src/main/java/apelet/association/service/ClueStatService.java new file mode 100644 index 0000000..d93a87b --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/service/ClueStatService.java @@ -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 getClueTransformStat(int pageNum, int pageSize); + + /** + * 查询线索转化统计整体汇总数据(不分线索来源,全量一行)。 + * 指标:转化商机金额、赢单商机金额、已转商机成交金额、线索总数、线索转化数、线索转化率、商机转化率。 + * + * @return 整体汇总统计结果 + */ + ClueStatSummaryVo getClueStatSummary(); +} diff --git a/common/common-association/src/main/java/apelet/association/service/impl/ClueStatServiceImpl.java b/common/common-association/src/main/java/apelet/association/service/impl/ClueStatServiceImpl.java new file mode 100644 index 0000000..126b6f0 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/service/impl/ClueStatServiceImpl.java @@ -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 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 getClueTransformStat(int pageNum, int pageSize) { + // 0. 兜底处理非法分页参数(页码从1开始、每页至少1条) + int safePageNum = Math.max(pageNum, 1); + int safePageSize = Math.max(pageSize, 1); + + // 1. 查询每种来源的线索总数 + List clueTotalList = memberClueMapper.selectClueTotalBySource(); + Map clueTotalMap = new HashMap<>(); + for (ClueSourceCountVo item : clueTotalList) { + clueTotalMap.put(item.getSource(), item.getClueTotalCount()); + } + + // 2. 查询每种来源的商机转化统计(转化商机数、赢单商机数、赢单商机金额、商机转化金额) + List oppStatList = businessOpportunitiesMapper.selectOppTransformStatBySource(); + Map oppStatMap = new HashMap<>(); + for (OppTransformStatVo item : oppStatList) { + oppStatMap.put(item.getSource(), item); + } + + // 3. 查询每种来源的成交统计(已转商机成交数、已转商机成交金额) + // 当前合同表 contract 暂无数据,暂以报价单 status='3'(通过,即已成合同)作为成交口径 + List dealStatList = businessOpportunitiesMapper.selectDealStatBySource(); + Map dealStatMap = new HashMap<>(); + for (DealTransformStatVo item : dealStatList) { + dealStatMap.put(item.getSource(), item); + } + + // 4. 固定返回4种来源的行,组装统计结果 + List resultList = new ArrayList<>(SOURCE_MAP.size()); + for (Map.Entry 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 paginate(List 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() + "%"; + } +} diff --git a/common/common-association/src/main/java/apelet/association/vo/ClueSourceCountVo.java b/common/common-association/src/main/java/apelet/association/vo/ClueSourceCountVo.java new file mode 100644 index 0000000..8668d68 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/vo/ClueSourceCountVo.java @@ -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; +} diff --git a/common/common-association/src/main/java/apelet/association/vo/ClueStatSummaryVo.java b/common/common-association/src/main/java/apelet/association/vo/ClueStatSummaryVo.java new file mode 100644 index 0000000..a4a0254 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/vo/ClueStatSummaryVo.java @@ -0,0 +1,56 @@ +package apelet.association.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 线索转化统计整体汇总结果对象(不分线索来源,全量一行) + *

+ * 指标口径: + * - 转化商机金额:所有关联到线索的商机 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; +} diff --git a/common/common-association/src/main/java/apelet/association/vo/ClueTransformStatVo.java b/common/common-association/src/main/java/apelet/association/vo/ClueTransformStatVo.java new file mode 100644 index 0000000..f173457 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/vo/ClueTransformStatVo.java @@ -0,0 +1,82 @@ +package apelet.association.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 线索转化统计结果对象(按线索来源维度展示每种线索的转化数据) + *

+ * 对应表头: + * 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; +} diff --git a/common/common-association/src/main/java/apelet/association/vo/DealTransformStatVo.java b/common/common-association/src/main/java/apelet/association/vo/DealTransformStatVo.java new file mode 100644 index 0000000..3f238df --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/vo/DealTransformStatVo.java @@ -0,0 +1,31 @@ +package apelet.association.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 按线索来源维度统计的"线索→商机→合同"成交中间结果 + *

+ * 说明:合同表 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; +} diff --git a/common/common-association/src/main/java/apelet/association/vo/OppTransformStatVo.java b/common/common-association/src/main/java/apelet/association/vo/OppTransformStatVo.java new file mode 100644 index 0000000..212e89e --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/vo/OppTransformStatVo.java @@ -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; +} diff --git a/common/common-association/src/main/resources/mapper/BusinessOpportunitiesMapper.xml b/common/common-association/src/main/resources/mapper/BusinessOpportunitiesMapper.xml new file mode 100644 index 0000000..5feb58b --- /dev/null +++ b/common/common-association/src/main/resources/mapper/BusinessOpportunitiesMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + diff --git a/common/common-association/src/main/resources/mapper/MemberClueMapper.xml b/common/common-association/src/main/resources/mapper/MemberClueMapper.xml new file mode 100644 index 0000000..cd789ce --- /dev/null +++ b/common/common-association/src/main/resources/mapper/MemberClueMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + +