From c824b2c0cfb3d0a0aa1323663322868210af2d5f Mon Sep 17 00:00:00 2001 From: hz <1283027134@qq.com> Date: Sun, 23 Aug 2026 12:45:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:3=E4=B8=AA=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/MemberClueReportController.java | 377 +++++++++++++++++++++ .../businessOpportunities/BusinessFromPlugin.java | 42 +++ .../plugin/member/MemberClueListPlugin.java | 14 + .../plugin/member/MemberInformationListPlugin.java | 58 ++++ 4 files changed, 491 insertions(+) create mode 100644 common/common-association/src/main/java/apelet/association/controller/MemberClueReportController.java create mode 100644 common/common-association/src/main/java/apelet/association/plugin/member/MemberClueListPlugin.java create mode 100644 common/common-association/src/main/java/apelet/association/plugin/member/MemberInformationListPlugin.java diff --git a/common/common-association/src/main/java/apelet/association/controller/MemberClueReportController.java b/common/common-association/src/main/java/apelet/association/controller/MemberClueReportController.java new file mode 100644 index 0000000..61cc414 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/controller/MemberClueReportController.java @@ -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 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 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> allList = new ArrayList<>(); + + for (int i = 0; i < opportunities.size(); i++) { + Map 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 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> pageList; + if (start >= total) { + pageList = new ArrayList<>(); + } else { + pageList = allList.subList(start, end); + } + + // 构建分页返回结果 + Map 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 stageCountMap = new LinkedHashMap<>(); + Map 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 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> stageList = new ArrayList<>(); + for (String stage : stages) { + int count = stageCountMap.get(stage); + BigDecimal amount = stageAmountMap.get(stage); + Map item = new LinkedHashMap<>(); + item.put("name", stage + count + "个"); + item.put("value", amount); + stageList.add(item); + } + + // 构建返回结果 + Map 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 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 + "小时"; + } + +} \ No newline at end of file diff --git a/common/common-association/src/main/java/apelet/association/plugin/businessOpportunities/BusinessFromPlugin.java b/common/common-association/src/main/java/apelet/association/plugin/businessOpportunities/BusinessFromPlugin.java index c129bbb..d61c1f5 100644 --- a/common/common-association/src/main/java/apelet/association/plugin/businessOpportunities/BusinessFromPlugin.java +++ b/common/common-association/src/main/java/apelet/association/plugin/businessOpportunities/BusinessFromPlugin.java @@ -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 { @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 { 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); + } + } } diff --git a/common/common-association/src/main/java/apelet/association/plugin/member/MemberClueListPlugin.java b/common/common-association/src/main/java/apelet/association/plugin/member/MemberClueListPlugin.java new file mode 100644 index 0000000..143d49b --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/plugin/member/MemberClueListPlugin.java @@ -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; + } +} diff --git a/common/common-association/src/main/java/apelet/association/plugin/member/MemberInformationListPlugin.java b/common/common-association/src/main/java/apelet/association/plugin/member/MemberInformationListPlugin.java new file mode 100644 index 0000000..8ad15b1 --- /dev/null +++ b/common/common-association/src/main/java/apelet/association/plugin/member/MemberInformationListPlugin.java @@ -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(); + } +} \ No newline at end of file