4 changed files with 491 additions and 0 deletions
@ -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 + "小时"; |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue