Browse Source

feat:3个报表

dev^2
hz 6 days ago
parent
commit
c824b2c0cf
  1. 377
      common/common-association/src/main/java/apelet/association/controller/MemberClueReportController.java
  2. 42
      common/common-association/src/main/java/apelet/association/plugin/businessOpportunities/BusinessFromPlugin.java
  3. 14
      common/common-association/src/main/java/apelet/association/plugin/member/MemberClueListPlugin.java
  4. 58
      common/common-association/src/main/java/apelet/association/plugin/member/MemberInformationListPlugin.java

377
common/common-association/src/main/java/apelet/association/controller/MemberClueReportController.java

@ -0,0 +1,377 @@ @@ -0,0 +1,377 @@
package apelet.association.controller;
import apelet.common.core.annotation.NoAuthInterface;
import apelet.common.core.object.ObjectCollection;
import apelet.common.core.object.ObjectValue;
import apelet.common.core.object.ResponseResult;
import apelet.common.generator.utils.OrmGenDataSourceUtil;
import apelet.common.orm.impl.*;
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;
import java.math.BigDecimal;
import java.util.*;
@RestController
@RequestMapping("/tenantadmin/clue")
public class MemberClueReportController {
@Autowired
private OrmGenDataSourceUtil ormGenDataSourceUtil;
/**
* 商机赢丢单分析
*/
@NoAuthInterface
@GetMapping("/winLostAnalysis")
public ResponseResult<?> winLostAnalysis() {
// 查询所有商机(deleted_flag = 1)
Filter filter = new Filter();
filter.add(new FilterItem("deleted_flag", FilterItem.equals, 1));
Selector selector = new Selector();
// 通过 getList() 添加字段
selector.getList().add(new SelectorItem("stage_manifestation"));
selector.getList().add(new SelectorItem("predicted_price"));
ObjectCollection opportunities = ormGenDataSourceUtil.query("business_opportunities", filter, selector, new Sorter());
// 统计数据
int totalCount = 0; // 商机总数
int winCount = 0; // 赢单数(签订合同 stage_manifestation = '5')
int lostCount = 0; // 丢单数(丢单 stage_manifestation = '6')
int otherCount = 0; // 其他数
BigDecimal winAmount = BigDecimal.ZERO; // 赢单金额
BigDecimal lostAmount = BigDecimal.ZERO; // 丢单金额
BigDecimal otherAmount = BigDecimal.ZERO; // 其他金额
for (int i = 0; i < opportunities.size(); i++) {
Map<String, Object> record = opportunities.getObject(i).getValues();
String stage = record.get("stage_manifestation") != null ? record.get("stage_manifestation").toString() : "";
BigDecimal price = record.get("predicted_price") != null ? new BigDecimal(record.get("predicted_price").toString()) : BigDecimal.ZERO;
totalCount++;
if ("5".equals(stage)) {
// 签订合同 = 赢单
winCount++;
winAmount = winAmount.add(price);
} else if ("6".equals(stage)) {
// 丢单
lostCount++;
lostAmount = lostAmount.add(price);
} else {
// 其他
otherCount++;
otherAmount = otherAmount.add(price);
}
}
// 计算比率(保留两位小数)
BigDecimal winRate = totalCount > 0 ? BigDecimal.valueOf(winCount)
.divide(BigDecimal.valueOf(totalCount), 4, BigDecimal.ROUND_HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(2, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
BigDecimal lostRate = totalCount > 0 ? BigDecimal.valueOf(lostCount)
.divide(BigDecimal.valueOf(totalCount), 4, BigDecimal.ROUND_HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(2, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
BigDecimal otherRate = totalCount > 0 ? BigDecimal.valueOf(otherCount)
.divide(BigDecimal.valueOf(totalCount), 4, BigDecimal.ROUND_HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(2, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
// 平均赢单金额
BigDecimal avgWinAmount = winCount > 0 ? winAmount.divide(BigDecimal.valueOf(winCount), 2, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
// 构建返回结果
Map<String, Object> result = new LinkedHashMap<>();
result.put("totalCount", totalCount); // 商机数
result.put("winCount", winCount); // 赢单数
result.put("winRate", winRate); // 赢单率(%)
result.put("winAmount", winAmount); // 赢单金额
result.put("avgWinAmount", avgWinAmount); // 平均赢单金额
result.put("lostCount", lostCount); // 丢单数
result.put("lostRate", lostRate); // 丢单率(%)
result.put("lostAmount", lostAmount); // 丢单金额
result.put("otherCount", otherCount); // 其他数
result.put("otherRate", otherRate); // 其他占比(%)
result.put("otherAmount", otherAmount); // 其他金额
return ResponseResult.success(result);
}
/**
* 商机阶段分布明细列表
*/
@NoAuthInterface
@GetMapping("/stageDistribution")
public ResponseResult<?> stageDistribution(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
// 查询所有商机(deleted_flag = 1)
Filter filter = new Filter();
filter.add(new FilterItem("deleted_flag", FilterItem.equals, 1));
// 按创建时间倒序
Sorter sorter = new Sorter();
SorterItem sorterItem = new SorterItem("create_time");
sorterItem.setSortType(1); // 降序
sorter.add(sorterItem);
// 查询全部数据(不分页),然后手动分页
ObjectCollection opportunities = ormGenDataSourceUtil.query("business_opportunities", filter, new Selector(), sorter);
// 构建完整列表
List<Map<String, Object>> allList = new ArrayList<>();
for (int i = 0; i < opportunities.size(); i++) {
Map<String, Object> record = opportunities.getObject(i).getValues();
// 获取客户名称
String unitName = getUnitName(record.get("unit_id"));
// 获取阶段名称
String stageName = getStageName(record.get("stage_manifestation"));
// 获取赢单率
String winRate = getWinRate(record.get("stage_manifestation"));
// 计算阶段时间
String stageDuration = getStageDuration(record.get("create_time"), record.get("actual_signdate"), record.get("stage_manifestation"));
Map<String, Object> item = new LinkedHashMap<>();
item.put("id", record.get("id")); // 不展示,供后续操作使用
item.put("name", record.get("name")); // 商机名称
item.put("number", record.get("number")); // 商机编码
item.put("unitName", unitName); // 客户
item.put("createTime", record.get("create_time")); // 商机日期
item.put("stageName", stageName); // 商机阶段
item.put("winRate", winRate); // 赢单率
item.put("stageDuration", stageDuration); // 阶段时间
allList.add(item);
}
// 手动分页
int total = allList.size();
int start = (pageNum - 1) * pageSize;
int end = Math.min(start + pageSize, total);
List<Map<String, Object>> pageList;
if (start >= total) {
pageList = new ArrayList<>();
} else {
pageList = allList.subList(start, end);
}
// 构建分页返回结果
Map<String, Object> result = new LinkedHashMap<>();
result.put("list", pageList);
result.put("total", total);
result.put("pageNum", pageNum);
result.put("pageSize", pageSize);
return ResponseResult.success(result);
}
/**
* 商机阶段分布统计按阶段分组统计数量 + 总金额 + 总计汇总
*/
@NoAuthInterface
@GetMapping("/stageStatistics")
public ResponseResult<?> stageStatistics() {
// 查询所有商机(deleted_flag = 1)
Filter filter = new Filter();
filter.add(new FilterItem("deleted_flag", FilterItem.equals, 1));
Selector selector = new Selector();
selector.getList().add(new SelectorItem("stage_manifestation"));
selector.getList().add(new SelectorItem("predicted_price"));
ObjectCollection opportunities = ormGenDataSourceUtil.query("business_opportunities", filter, selector, new Sorter());
// 统计各阶段数量 + 总金额
Map<String, Integer> stageCountMap = new LinkedHashMap<>();
Map<String, BigDecimal> stageAmountMap = new LinkedHashMap<>();
// 初始化所有阶段(保证返回顺序)
String[] stages = {"挖掘商机", "发展商机", "方案阶段", "商务谈判", "签约阶段"};
for (String stage : stages) {
stageCountMap.put(stage, 0);
stageAmountMap.put(stage, BigDecimal.ZERO);
}
// 总计
int totalCount = 0;
BigDecimal totalAmount = BigDecimal.ZERO;
for (int i = 0; i < opportunities.size(); i++) {
Map<String, Object> record = opportunities.getObject(i).getValues();
String stageValue = record.get("stage_manifestation") != null ? record.get("stage_manifestation").toString() : "";
BigDecimal price = record.get("predicted_price") != null ? new BigDecimal(record.get("predicted_price").toString()) : BigDecimal.ZERO;
String stageName = getStageName(stageValue);
if (stageCountMap.containsKey(stageName)) {
stageCountMap.put(stageName, stageCountMap.get(stageName) + 1);
stageAmountMap.put(stageName, stageAmountMap.get(stageName).add(price));
}
// 累加总计(所有商机都算,包括丢单)
totalCount++;
totalAmount = totalAmount.add(price);
}
// 构建阶段分布列表(name: 阶段名+数量, value: 总金额)
List<Map<String, Object>> stageList = new ArrayList<>();
for (String stage : stages) {
int count = stageCountMap.get(stage);
BigDecimal amount = stageAmountMap.get(stage);
Map<String, Object> item = new LinkedHashMap<>();
item.put("name", stage + count + "个");
item.put("value", amount);
stageList.add(item);
}
// 构建返回结果
Map<String, Object> result = new LinkedHashMap<>();
result.put("totalCount", totalCount);
result.put("totalAmount", totalAmount);
result.put("stageList", stageList);
return ResponseResult.success(result);
}
/**
* 根据阶段字典值获取阶段名称
*/
private String getStageName(Object stageValue) {
if (stageValue == null) {
return "";
}
String value = stageValue.toString();
switch (value) {
case "1":
return "挖掘商机";
case "2":
return "发展商机";
case "3":
return "方案阶段";
case "4":
return "商务谈判";
case "5":
case "6":
return "签约阶段";
default:
return "";
}
}
/**
* 根据 unit_id 查询单位名称
*/
private String getUnitName(Object unitId) {
if (unitId == null) {
return "";
}
// unit_id 是外键字段,查询结果中是 F7 对象 {id:xxx},先抽出 id 再按主键查询
if (unitId instanceof Map) {
Object idObj = ((Map<?, ?>) unitId).get("id");
if (idObj == null) {
return "";
}
unitId = idObj;
} else if (unitId instanceof ObjectValue) {
Object idObj = ((ObjectValue) unitId).get("id");
if (idObj == null) {
return "";
}
unitId = idObj;
}
try {
Filter filter = new Filter();
filter.add(new FilterItem("id", FilterItem.equals, unitId));
ObjectCollection unit = ormGenDataSourceUtil.query("unit", filter, new Selector());
if (unit != null && unit.size() > 0) {
Map<String, Object> unitMap = unit.getObject(0).getValues();
Object name = unitMap.get("name");
return name != null ? name.toString() : "";
}
} catch (Exception e) {
// 查不到返回空串
}
return "";
}
/**
* 根据阶段获取赢单率
*/
private String getWinRate(Object stageValue) {
if (stageValue == null) {
return "0%";
}
String value = stageValue.toString();
switch (value) {
case "1":
return "10%";
case "2":
return "30%";
case "3":
return "50%";
case "4":
return "70%";
case "5":
return "100%";
default:
return "0%";
}
}
/**
* 计算阶段时间X天X小时
*/
private String getStageDuration(Object createTimeObj, Object actualSignDateObj, Object stageValue) {
if (createTimeObj == null) {
return "0天0小时";
}
Date createTime = (Date) createTimeObj;
Date endTime;
String value = stageValue != null ? stageValue.toString() : "";
// 签订合同(5)和丢单(6)都用实际签单日期
if ("5".equals(value) || "6".equals(value)) {
if (actualSignDateObj != null) {
endTime = (Date) actualSignDateObj;
} else {
return "-";
}
} else {
endTime = new Date();
}
long diffMillis = endTime.getTime() - createTime.getTime();
if (diffMillis < 0) {
return "0天0小时";
}
long diffDays = diffMillis / (1000 * 60 * 60 * 24);
long diffHours = (diffMillis % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60);
return diffDays + "天" + diffHours + "小时";
}
}

42
common/common-association/src/main/java/apelet/association/plugin/businessOpportunities/BusinessFromPlugin.java

@ -16,6 +16,12 @@ public class BusinessFromPlugin extends ExecutePluginParent { @@ -16,6 +16,12 @@ public class BusinessFromPlugin extends ExecutePluginParent {
if (objectValue.get("billstatus") == null) {
this.setWidgetAttribute("billstatus", AttributeEnum.VALUE_CHANGE, "A");
}
// 页面打开时按已有阶段初始化日期字段的显示/必填(formCreated 的 objectValue 用驼峰变量名)
String stage = objectValue.getString("stageManifestation");
if (stage == null || stage.isEmpty()) {
stage = objectValue.getString("stage_manifestation");
}
applyStageShowHide(stage);
}
/**
@ -26,6 +32,10 @@ public class BusinessFromPlugin extends ExecutePluginParent { @@ -26,6 +32,10 @@ public class BusinessFromPlugin extends ExecutePluginParent {
@Override
public void change(String widgetVariableName, ObjectValue objectValue) {
super.change(widgetVariableName, objectValue);
if ("stageManifestation".equals(widgetVariableName)) {
handleStageChange(objectValue);
return;
}
if (!"clueId".equals(widgetVariableName)) {
return;
}
@ -77,4 +87,36 @@ public class BusinessFromPlugin extends ExecutePluginParent { @@ -77,4 +87,36 @@ public class BusinessFromPlugin extends ExecutePluginParent {
this.setWidgetAttribute(widgetVariableName, AttributeEnum.VALUE_CHANGE, value);
}
}
/**
* 处理阶段变更控制 actualSigndate lostSigndate 的显示/隐藏和必填
*/
private void handleStageChange(ObjectValue objectValue) {
applyStageShowHide(objectValue.getString("stage_manifestation"));
}
/**
* 按阶段值设置 actualSigndate/lostSigndate 的显示与必填
* 签订合同5显示 actualSigndate隐藏 lostSigndate必填
* 丢单6显示 lostSigndate隐藏 actualSigndate必填
* 其他两个都隐藏非必填
*/
private void applyStageShowHide(String stage) {
if ("5".equals(stage)) {
this.setWidgetAttribute("actualSigndate", AttributeEnum.SHOW, true);
this.setWidgetAttribute("actualSigndate", AttributeEnum.REQUIRED, true);
this.setWidgetAttribute("lostSigndate", AttributeEnum.SHOW, false);
this.setWidgetAttribute("lostSigndate", AttributeEnum.REQUIRED, false);
} else if ("6".equals(stage)) {
this.setWidgetAttribute("lostSigndate", AttributeEnum.SHOW, true);
this.setWidgetAttribute("lostSigndate", AttributeEnum.REQUIRED, true);
this.setWidgetAttribute("actualSigndate", AttributeEnum.SHOW, false);
this.setWidgetAttribute("actualSigndate", AttributeEnum.REQUIRED, false);
} else {
this.setWidgetAttribute("actualSigndate", AttributeEnum.SHOW, false);
this.setWidgetAttribute("actualSigndate", AttributeEnum.REQUIRED, false);
this.setWidgetAttribute("lostSigndate", AttributeEnum.SHOW, false);
this.setWidgetAttribute("lostSigndate", AttributeEnum.REQUIRED, false);
}
}
}

14
common/common-association/src/main/java/apelet/association/plugin/member/MemberClueListPlugin.java

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
package apelet.association.plugin.member;
import apelet.common.online.abstractplugin.ListPlugin;
import apelet.common.orm.impl.Filter;
import apelet.common.orm.impl.FilterItem;
public class MemberClueListPlugin extends ListPlugin {
@Override
protected Filter getFilter() {
Filter filter = new Filter();
filter.add(new FilterItem("deleted_flag", FilterItem.equals, 1));
return filter;
}
}

58
common/common-association/src/main/java/apelet/association/plugin/member/MemberInformationListPlugin.java

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
package apelet.association.plugin.member;
import apelet.common.core.object.ObjectValue;
import apelet.common.core.util.ApplicationContextHolder;
import apelet.common.online.abstractplugin.ListPlugin;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
/**
* 会员信息列表插件
* 按三个阶段统计潜在会员存量会员流失会员
*/
@Slf4j
public class MemberInformationListPlugin extends ListPlugin {
@Override
public String getSql(ObjectValue objectValue) {
log.info("========== MemberInformationListPlugin.getSql 被调用了 ==========");
StringBuilder sql = new StringBuilder();
// 1. 潜在会员:member_clue(待分配、放弃中、跟进中)
sql.append("SELECT");
sql.append(" '潜在会员' AS stage,");
sql.append(" t.name AS customer_name,");
sql.append(" u.show_name AS charge_person");
sql.append(" FROM member_clue t");
sql.append(" LEFT JOIN xy_sys_user u ON u.id = t.create_user_id AND u.deleted_flag = 1");
sql.append(" WHERE t.deleted_flag = 1");
sql.append(" AND t.clue_type IN ('0', '1', '3')");
sql.append(" UNION ALL");
// 2. membership_apply:潜在会员(A/B)+ 存量会员(C且未逾期)+ 流失会员(C且逾期)
sql.append(" SELECT");
sql.append(" CASE");
sql.append(" WHEN t.billstatus = 'A' OR t.billstatus = 'B' THEN '潜在会员'");
sql.append(" WHEN t.billstatus = 'C' AND (t.overdue IS NULL OR t.overdue = 0) THEN '存量会员'");
sql.append(" WHEN t.billstatus = 'C' AND t.overdue = 1 THEN '流失会员'");
sql.append(" ELSE NULL");
sql.append(" END AS stage,");
sql.append(" t.unit_name AS customer_name,");
sql.append(" u.show_name AS charge_person");
sql.append(" FROM membership_apply t");
sql.append(" LEFT JOIN xy_sys_user u ON u.id = t.membership_manger_id AND u.deleted_flag = 1");
sql.append(" WHERE t.deleted_flag = 1");
sql.append(" AND t.billstatus IS NOT NULL");
sql.append(" AND (");
sql.append(" t.billstatus IN ('A', 'B')");
sql.append(" OR (t.billstatus = 'C' AND (t.overdue IS NULL OR t.overdue = 0))");
sql.append(" OR (t.billstatus = 'C' AND t.overdue = 1)");
sql.append(" )");
log.info("会员信息列表SQL:{}", sql.toString());
return sql.toString();
}
}
Loading…
Cancel
Save