4 changed files with 491 additions and 0 deletions
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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