Преглед изворни кода

更新我的面板的统计信息

沐梦. пре 1 месец
родитељ
комит
4c0e1a47ee

+ 6 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerInfoVo.java

@@ -267,4 +267,10 @@ public class CustomerInfoVo implements Serializable {
     private String customerLevelName;
     /** 合作状态名称 */
     private String cooperationName;
+    /** 客户来源名称 */
+    private String customerSourceName;
+    /** 开票类型 */
+    private String invoiceType;
+    /** 开票类型名称 */
+    private String invoiceTypeName;
 }

+ 1 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerSalesInfoVo.java

@@ -192,5 +192,6 @@ public class CustomerSalesInfoVo implements Serializable {
 
     private Long settlementMethod;
 
+    private String creditLevel;
 
 }

+ 27 - 18
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/WorkbenchStatVo.java

@@ -1,7 +1,6 @@
 package org.dromara.customer.domain.vo;
 
 import lombok.Data;
-
 import java.util.List;
 
 /**
@@ -10,29 +9,31 @@ import java.util.List;
 @Data
 public class WorkbenchStatVo {
 
-    /**
-     * 商机统计
-     */
-    private List<StatItem> opportunityStats;
+    /** 客户统计 */
+    private List<StatItem> customerStats;
+
+    /** 客户跟进统计 */
+    private List<StatItem> followUpStats;
+
+    /** 项目商机统计 (总览) */
+    private List<StatItem> opportunityTotalStats;
+
+    /** 跟进中的项目商机 */
+    private List<StatItem> opportunityInFollowStats;
+
+    /** 年度入围统计 (总览) */
+    private List<StatItem> selectionTotalStats;
 
-    /**
-     * 年度入围统计
-     */
-    private List<StatItem> selectionStats;
+    /** 跟进中的年度入围 */
+    private List<StatItem> selectionInFollowStats;
 
-    /**
-     * 商机漏斗
-     */
+    /** 商机漏斗 */
     private List<StatItem> opportunityFunnel;
 
-    /**
-     * 客户漏斗
-     */
+    /** 客户漏斗 */
     private List<StatItem> customerFunnel;
 
-    /**
-     * 访销统计
-     */
+    /** 访销统计 */
     private List<StatItem> visitStats;
 
     @Data
@@ -40,12 +41,20 @@ public class WorkbenchStatVo {
         private String label;
         private String value;
         private String unit;
+        /** 较上周涨跌比例 */
+        private Double trend;
 
         public StatItem(String label, String value) {
             this.label = label;
             this.value = value;
         }
 
+        public StatItem(String label, String value, Double trend) {
+            this.label = label;
+            this.value = value;
+            this.trend = trend;
+        }
+
         public StatItem(String label, String value, String unit) {
             this.label = label;
             this.value = value;

+ 119 - 68
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerInfoServiceImpl.java

@@ -96,6 +96,7 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
     private final CustomerContractMapper customerContractMapper;
     private final CustomerDeptMapper customerDeptMapper;
     private final CustomerDictMapper customerDictMapper;
+    private final SupplierBusinessInfoMapper supplierBusinessInfoMapper;
 
 
     /**
@@ -115,99 +116,149 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
         if (vo == null) {
             return null;
         }
-        EnterpriseScaleVo enterpriseScaleVo = enterpriseScaleMapper.selectVoById(vo.getEnterpriseScaleId());
-        if (enterpriseScaleVo != null) {
-            vo.setEnterpriseScale(enterpriseScaleVo.getEnterpriseScaleName());
-        }
 
-        IndustryCategoryVo industryCategoryVo = industryCategoryMapper.selectVoById(vo.getIndustryCategoryId());
-        if (industryCategoryVo != null) {
-            vo.setIndustryCategory(industryCategoryVo.getIndustryCategoryName());
+        // 补充基础名称字段
+        if (vo.getEnterpriseScaleId() != null) {
+            EnterpriseScaleVo enterpriseScaleVo = enterpriseScaleMapper.selectVoById(vo.getEnterpriseScaleId());
+            if (enterpriseScaleVo != null) {
+                vo.setEnterpriseScale(enterpriseScaleVo.getEnterpriseScaleName());
+            }
         }
-        // 2. 查询关联信息
-        vo.setCustomerBusinessInfoVo(
-            Optional.ofNullable(customerBusinessInfoMapper.selectByCustomerId(id))
-                .map(entity -> MapstructUtils.convert(entity, CustomerBusinessInfoVo.class))
-                .orElse(null)
-        );
 
-        CustomerSalesInfo customerSalesInfo = customerSalesInfoMapper.selectByCustomerId(id);
-        if (customerSalesInfo == null) {
-            vo.setCustomerSalesInfoVo(null);
-            return vo;
+        if (vo.getIndustryCategoryId() != null) {
+            IndustryCategoryVo industryCategoryVo = industryCategoryMapper.selectVoById(vo.getIndustryCategoryId());
+            if (industryCategoryVo != null) {
+                vo.setIndustryCategory(industryCategoryVo.getIndustryCategoryName());
+                vo.setIndustryName(industryCategoryVo.getIndustryCategoryName());
+            }
         }
-        if (ObjectUtils.isNotEmpty(customerSalesInfo.getCreditAmount()) && ObjectUtils.isNotEmpty(customerSalesInfo.getTemporaryQuota())) {
-            customerSalesInfo.setRemainingQuota(customerSalesInfo.getCreditAmount().add(customerSalesInfo.getTemporaryQuota()));
+
+        // 企业类型名称 (Q0001)
+        if (vo.getCustomerTypeId() != null) {
+            CustomerDict dict = customerDictMapper.selectById(vo.getCustomerTypeId());
+            if (dict != null) {
+                vo.setEnterpriseTypeName(dict.getName());
+            }
         }
-        Set<Long> staffIds = new HashSet<>();
-        Set<Long> deptIds = new HashSet<>();
 
-        if (vo.getSalesPersonId() != null) {
-            staffIds.add(vo.getSalesPersonId());
+        // 客户等级名称 (远程调用)
+        if (vo.getCustomerLevelId() != null) {
+            Map<Long, String> levelMap = remoteComCustomerLevelService.selectCustomerLevelNameByIds(Collections.singleton(vo.getCustomerLevelId()));
+            vo.setCustomerLevelName(levelMap.get(vo.getCustomerLevelId()));
         }
-        if (vo.getServiceStaffId() != null) {
-            staffIds.add(vo.getServiceStaffId());
+
+        // 合作状态名称 (COOPERATION_STATUS)
+        if (StringUtils.isNotBlank(vo.getStatus())) {
+            List<CustomerDict> cooperationDicts = customerDictMapper.selectList(new LambdaQueryWrapper<CustomerDict>()
+                .eq(CustomerDict::getCode, "COOPERATION_STATUS"));
+            Map<String, String> cooperationMap = cooperationDicts.stream()
+                .collect(Collectors.toMap(
+                    d -> {
+                        String val = d.getValue();
+                        return (val == null || val.isEmpty()) ? (d.getCodeIndex() != null ? String.valueOf(d.getCodeIndex()) : "") : val;
+                    },
+                    CustomerDict::getName,
+                    (k1, k2) -> k1
+                ));
+            vo.setCooperationName(cooperationMap.get(vo.getStatus()));
+        }
+
+        // 2. 查询关联信息 - 工商信息 (增加供应商表作为备选)
+        CustomerBusinessInfo businessInfo = customerBusinessInfoMapper.selectByCustomerId(id);
+        if (businessInfo == null) {
+            // 尝试从供应商工商信息表查询
+            SupplierBusinessInfo supplierBiz = supplierBusinessInfoMapper.selectById(id);
+            if (supplierBiz != null) {
+                businessInfo = BeanUtil.toBean(supplierBiz, CustomerBusinessInfo.class);
+                businessInfo.setCustomerId(id);
+                // 字段名映射差异处理
+                businessInfo.setBusinessCustomerName(supplierBiz.getBusinessName());
+            }
         }
-        if (vo.getBelongingDepartmentId() != null) {
-            deptIds.add(vo.getBelongingDepartmentId());
+
+        if (businessInfo != null) {
+            vo.setCustomerBusinessInfoVo(MapstructUtils.convert(businessInfo, CustomerBusinessInfoVo.class));
+            // 补充主表缺失字段
+            if (StringUtils.isBlank(vo.getSocialCreditCode())) vo.setSocialCreditCode(businessInfo.getSocialCreditCode());
+            if (StringUtils.isBlank(vo.getBusinessCustomerName())) vo.setBusinessCustomerName(businessInfo.getBusinessCustomerName());
         }
 
-        Map<Long, String> staffMap = staffIds.isEmpty()
-            ? Collections.emptyMap()
-            : remoteErpStaffService.selectStaffNameByIds(staffIds);
+        // 销售信息
+        CustomerSalesInfo customerSalesInfo = customerSalesInfoMapper.selectByCustomerId(id);
+        if (customerSalesInfo != null) {
+            if (ObjectUtils.isNotEmpty(customerSalesInfo.getCreditAmount()) && ObjectUtils.isNotEmpty(customerSalesInfo.getTemporaryQuota())) {
+                customerSalesInfo.setRemainingQuota(customerSalesInfo.getCreditAmount().add(customerSalesInfo.getTemporaryQuota()));
+            }
 
-        Map<Long, String> deptMap = deptIds.isEmpty()
-            ? Collections.emptyMap()
-            : remoteErpDeptService.selectDeptNameByIds(deptIds);
+            CustomerSalesInfoVo voObj = MapstructUtils.convert(customerSalesInfo, CustomerSalesInfoVo.class);
+
+            // 补充客户来源名称 (K0001)
+            if (StringUtils.isNotBlank(customerSalesInfo.getCustomerSource())) {
+                 CustomerDict sourceDict = customerDictMapper.selectOne(new LambdaQueryWrapper<CustomerDict>()
+                     .eq(CustomerDict::getCode, "K0001")
+                     .eq(CustomerDict::getValue, customerSalesInfo.getCustomerSource()));
+                 if (sourceDict == null && StringUtils.isNumeric(customerSalesInfo.getCustomerSource())) {
+                     // 尝试作为 ID 查询
+                     sourceDict = customerDictMapper.selectById(Long.parseLong(customerSalesInfo.getCustomerSource()));
+                 }
+                 if (sourceDict != null) {
+                     vo.setCustomerSourceName(sourceDict.getName());
+                 }
+            }
 
-        // 先转换
-        CustomerSalesInfoVo voObj = MapstructUtils.convert(customerSalesInfo, CustomerSalesInfoVo.class);
+            // 补充人员和部门名称
+            Set<Long> staffIds = new HashSet<>();
+            Set<Long> deptIds = new HashSet<>();
+            if (vo.getSalesPersonId() != null) staffIds.add(vo.getSalesPersonId());
+            if (vo.getServiceStaffId() != null) staffIds.add(vo.getServiceStaffId());
+            if (vo.getBelongingDepartmentId() != null) deptIds.add(vo.getBelongingDepartmentId());
+
+            Map<Long, String> staffMap = staffIds.isEmpty() ? Collections.emptyMap() : remoteErpStaffService.selectStaffNameByIds(staffIds);
+            Map<Long, String> deptMap = deptIds.isEmpty() ? Collections.emptyMap() : remoteErpDeptService.selectDeptNameByIds(deptIds);
 
-        // 再补充名称
-        if (voObj != null) {
             vo.setSalesPersonName(staffMap.get(vo.getSalesPersonId()));
             vo.setServiceStaffName(staffMap.get(vo.getServiceStaffId()));
             vo.setBelongingDepartmentName(deptMap.get(vo.getBelongingDepartmentId()));
 
-            voObj.setSalesPerson(staffMap.get(vo.getSalesPersonId()));
-            voObj.setServiceStaff(staffMap.get(vo.getServiceStaffId()));
-            voObj.setBelongingDepartment(deptMap.get(vo.getBelongingDepartmentId()));
+            if (voObj != null) {
+                voObj.setSalesPerson(staffMap.get(vo.getSalesPersonId()));
+                voObj.setServiceStaff(staffMap.get(vo.getServiceStaffId()));
+                voObj.setBelongingDepartment(deptMap.get(vo.getBelongingDepartmentId()));
+                // 信用等级名称
+                if (voObj.getCreditLevelId() != null) {
+                    Map<Long, String> creditMap = remoteCreditLevelService.selectCreditLevelNameByIds(Collections.singleton(voObj.getCreditLevelId()));
+                    voObj.setCreditLevel(creditMap.get(voObj.getCreditLevelId()));
+                }
+            }
+            vo.setCustomerSalesInfoVo(voObj);
         }
 
-        vo.setCustomerSalesInfoVo(voObj);
+        // 补充开票类型 (来自主表 sell_invoice_type,独立于销售信息表)
+        vo.setInvoiceType(vo.getSellInvoiceType());
+        vo.setInvoiceTypeName(vo.getSellInvoiceType());
 
+        // 联系人列表
         List<CustomerContact> contactEntities = customerContactMapper.selectListByCustomerId(id);
-        vo.setCustomerContactVoList(
-            contactEntities != null
-                ? contactEntities.stream()
-                .map(contact -> MapstructUtils.convert(contact, CustomerContactVo.class))
-                .collect(Collectors.toList())
-                : Collections.emptyList()
-        );
+        vo.setCustomerContactVoList(CollUtil.isEmpty(contactEntities) ? Collections.emptyList() :
+            contactEntities.stream().map(c -> MapstructUtils.convert(c, CustomerContactVo.class)).collect(Collectors.toList()));
 
+        // 发票列表
         List<CustomerInvoiceInfo> invoiceEntities = customerInvoiceInfoMapper.selectListByCustomerId(id);
-        vo.setCustomerInvoiceInfoVoList(
-            invoiceEntities != null
-                ? invoiceEntities.stream()
-                .map(invoice -> MapstructUtils.convert(invoice, CustomerInvoiceInfoVo.class))
-                .collect(Collectors.toList())
-                : Collections.emptyList()
-        );
+        vo.setCustomerInvoiceInfoVoList(CollUtil.isEmpty(invoiceEntities) ? Collections.emptyList() :
+            invoiceEntities.stream().map(i -> MapstructUtils.convert(i, CustomerInvoiceInfoVo.class)).collect(Collectors.toList()));
+
+        // 3. 计算部门额度
         Long userId = LoginHelper.getLoginUser().getUserId();
-        // 2. 使用 Optional 和 Stream 查找并处理
-        if (Objects.nonNull(userId)) { // 或者使用 userId != null
-            Optional<CustomerContact> optionalContact = contactEntities.stream()
+        if (Objects.nonNull(userId) && CollUtil.isNotEmpty(contactEntities)) {
+            contactEntities.stream()
                 .filter(contactEntity -> Objects.equals(contactEntity.getUserId(), userId))
-                .findFirst();
-
-            // 3. 只有当找到匹配的 contact 时,才执行后续逻辑
-            optionalContact.ifPresent(contact -> {
-                CustomerDeptVo customerDeptVo = customerDeptMapper.selectVoById(contact.getDeptId());
-                // 建议增加对 customerDeptVo 的非空判断,防止 selectVoById 返回 null
-                if (customerDeptVo != null) {
-                    vo.setDeptCredit(customerDeptVo.getResidueYearlyBudget());
-                }
-            });
+                .findFirst()
+                .ifPresent(contact -> {
+                    CustomerDeptVo customerDeptVo = customerDeptMapper.selectVoById(contact.getDeptId());
+                    if (customerDeptVo != null) {
+                        vo.setDeptCredit(customerDeptVo.getResidueYearlyBudget());
+                    }
+                });
         }
         return vo;
     }

+ 202 - 199
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/WorkbenchServiceImpl.java

@@ -26,248 +26,251 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
     private final CustomerSalesInfoMapper customerSalesInfoMapper;
     private final SalesresultanalyzeMapper salesresultanalyzeMapper;
 
+    private static final long DAY_MS = 24 * 3600000L;
+
     @Override
     public WorkbenchStatVo getStat() {
         WorkbenchStatVo vo = new WorkbenchStatVo();
-        vo.setOpportunityStats(getOpportunityStats());
-        vo.setSelectionStats(getSelectionStats());
+        vo.setCustomerStats(getCustomerStats());
+        vo.setFollowUpStats(getFollowUpStats());
+        vo.setOpportunityTotalStats(getOpportunityTotalStats());
+        vo.setOpportunityInFollowStats(getOpportunityInFollowStats());
+        vo.setSelectionTotalStats(getSelectionTotalStats());
+        vo.setSelectionInFollowStats(getSelectionInFollowStats());
         vo.setOpportunityFunnel(getOpportunityFunnel());
         vo.setCustomerFunnel(getCustomerFunnel());
         vo.setVisitStats(getVisitStats());
         return vo;
     }
 
-    private List<WorkbenchStatVo.StatItem> getOpportunityFunnel() {
-        // 1. 销售线索:iz_clue = 1
-        Long clueCount = salesleadsMapper.selectCount(new LambdaQueryWrapper<Salesleads>()
-            .eq(Salesleads::getIzClue, 1)
-            .eq(Salesleads::getDelFlag, "0"));
-
-        // 2. 商机:iz_clue = 0
-        Long opportunityCount = salesleadsMapper.selectCount(new LambdaQueryWrapper<Salesleads>()
-            .eq(Salesleads::getIzClue, 0)
-            .eq(Salesleads::getDelFlag, "0"));
-
-        // 3. 赢单:salesresultanalyze 中 dealResult = 1, dataType = 2 (商机)
-        Long winCount = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
-            .eq(SalesResultAnalyze::getDealResult, 1)
-            .eq(SalesResultAnalyze::getDataType, 2)
-            .eq(SalesResultAnalyze::getIsDelete, 0));
+    // 趋势计算通用工具:限制在 [-1.0, 1.0] 之间 (即最高 100%)
+    private Double calculateTrend(long current, long previous) {
+        if (previous <= 0) return current > 0 ? 1.0 : 0.0;
+        double res = (double) (current - previous) / previous;
+        return Math.max(-1.0, Math.min(1.0, res));
+    }
 
-        List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
-        stats.add(new WorkbenchStatVo.StatItem("销售线索", String.valueOf(clueCount), "个"));
-        stats.add(new WorkbenchStatVo.StatItem("商机", String.valueOf(opportunityCount), "个"));
-        stats.add(new WorkbenchStatVo.StatItem("赢单", String.valueOf(winCount), "个"));
-        return stats;
+    private Double calculateTrend(BigDecimal current, BigDecimal previous) {
+        if (previous == null || previous.compareTo(BigDecimal.ZERO) <= 0) {
+            return (current != null && current.compareTo(BigDecimal.ZERO) > 0) ? 1.0 : 0.0;
+        }
+        double res = current.subtract(previous).divide(previous, 4, BigDecimal.ROUND_HALF_UP).doubleValue();
+        return Math.max(-1.0, Math.min(1.0, res));
     }
 
-    private List<WorkbenchStatVo.StatItem> getCustomerFunnel() {
-        // 1. 正式客户:在 sales_info 表中 sales_person_id 和 service_staff_id 都不为空
-        Long officialCount = customerSalesInfoMapper.selectCount(new LambdaQueryWrapper<CustomerSalesInfo>()
-            .isNotNull(CustomerSalesInfo::getSalesPersonId)
-            .isNotNull(CustomerSalesInfo::getServiceStaffId)
-            .eq(CustomerSalesInfo::getDelFlag, "0"));
+    // 获取各维度的历史计数(7天前)
+    private long countBefore(Date date, String delFlag, boolean isOfficial) {
+        LambdaQueryWrapper<CustomerInfo> wrapper = new LambdaQueryWrapper<CustomerInfo>()
+            .le(CustomerInfo::getCreateTime, date).eq(CustomerInfo::getDelFlag, delFlag);
+        if (isOfficial) {
+            wrapper.isNotNull(CustomerInfo::getSalesPersonId);
+        }
+        return customerInfoMapper.selectCount(wrapper);
+    }
 
-        // 2. 公海客户:总客户数 - 正式客户数 (或者只要有一个为空即为公海)
-        Long totalCount = customerInfoMapper.selectCount(new LambdaQueryWrapper<CustomerInfo>()
-            .eq(CustomerInfo::getDelFlag, "0"));
+    // --- 1. 客户统计 ---
+    private List<WorkbenchStatVo.StatItem> getCustomerStats() {
+        Date now = new Date();
+        Date weekAgo = new Date(now.getTime() - 7 * DAY_MS);
         
-        Long publicCount = Math.max(0L, totalCount - officialCount);
-
-        // 3. 已成交:这里采用 Salesresultanalyze 中已成交的记录数作为“已成交”阶段的量度
-        Long dealCount = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
-            .eq(SalesResultAnalyze::getDealResult, 1)
-            .eq(SalesResultAnalyze::getIsDelete, 0));
+        // 精确获取本月初 00:00:00
+        Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        Date monthStart = cal.getTime();
+
+        // 当前值
+        long total = countBefore(now, "0", false);
+        long official = countBefore(now, "0", true);
+        long publicPool = total - official;
+        long newMonth = customerInfoMapper.selectCount(new LambdaQueryWrapper<CustomerInfo>()
+            .ge(CustomerInfo::getCreateTime, monthStart).eq(CustomerInfo::getDelFlag, "0"));
+        long dealMonth = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
+            .eq(SalesResultAnalyze::getDealResult, 1).ge(SalesResultAnalyze::getCreateTime, monthStart).eq(SalesResultAnalyze::getIsDelete, 0));
+
+        // 7天前值
+        long totalOld = countBefore(weekAgo, "0", false);
+        long offOld = countBefore(weekAgo, "0", true);
+        
+        // 增量指标趋势:过去7天 vs 再往前7天
+        Date twoWeeksAgo = new Date(now.getTime() - 14 * DAY_MS);
+        long newThisWeek = customerInfoMapper.selectCount(new LambdaQueryWrapper<CustomerInfo>().ge(CustomerInfo::getCreateTime, weekAgo).eq(CustomerInfo::getDelFlag, "0"));
+        long newLastWeek = customerInfoMapper.selectCount(new LambdaQueryWrapper<CustomerInfo>().ge(CustomerInfo::getCreateTime, twoWeeksAgo).lt(CustomerInfo::getCreateTime, weekAgo).eq(CustomerInfo::getDelFlag, "0"));
 
         List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
-        stats.add(new WorkbenchStatVo.StatItem("公海客户数", String.valueOf(publicCount), "个"));
-        stats.add(new WorkbenchStatVo.StatItem("正式客户", String.valueOf(officialCount), "个"));
-        stats.add(new WorkbenchStatVo.StatItem("已成交", String.valueOf(dealCount), "个"));
+        stats.add(new WorkbenchStatVo.StatItem("公海客户数", String.valueOf(publicPool), calculateTrend(publicPool, totalOld - offOld)));
+        stats.add(new WorkbenchStatVo.StatItem("正式客户数量", String.valueOf(official), calculateTrend(official, offOld)));
+        stats.add(new WorkbenchStatVo.StatItem("本月新增", String.valueOf(newMonth), calculateTrend(newThisWeek, newLastWeek)));
+        stats.add(new WorkbenchStatVo.StatItem("本月成交", String.valueOf(dealMonth), 0.0));
         return stats;
     }
 
-    private List<WorkbenchStatVo.StatItem> getVisitStats() {
-        // 统计本月数据
-        Calendar calendar = Calendar.getInstance();
-        calendar.set(Calendar.DAY_OF_MONTH, 1);
-        calendar.set(Calendar.HOUR_OF_DAY, 0);
-        calendar.set(Calendar.MINUTE, 0);
-        calendar.set(Calendar.SECOND, 0);
-        Date monthStart = calendar.getTime();
-
-        // 1. 拜访客户数:从 followuplog 中统计本月去重的客户
-        List<FollowUpLog> visitRecords = followUpLogMapper.selectList(new LambdaQueryWrapper<FollowUpLog>()
-            .ge(FollowUpLog::getCallDate, monthStart)
-            .eq(FollowUpLog::getIsDelete, 0));
-        long visitCustomerCount = visitRecords.stream()
-            .map(FollowUpLog::getCustomerName)
-            .filter(Objects::nonNull)
-            .distinct()
-            .count();
-
-        // 2. 拜访次数
-        int visitCount = visitRecords.size();
-
-        // 3. 销售金额:赢单项目的预算总和
-        List<SalesResultAnalyze> winList = salesresultanalyzeMapper.selectList(new LambdaQueryWrapper<SalesResultAnalyze>()
-            .eq(SalesResultAnalyze::getDealResult, 1)
-            .eq(SalesResultAnalyze::getIsDelete, 0));
-
-        BigDecimal totalAmount = BigDecimal.ZERO;
-        if (!winList.isEmpty()) {
-            List<String> winNos = winList.stream().map(SalesResultAnalyze::getObjectNo).collect(Collectors.toList());
-            List<Salesleads> wins = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>()
-                .in(Salesleads::getProjectNo, winNos)
-                .eq(Salesleads::getDelFlag, "0"));
-            totalAmount = wins.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+    // --- 2. 客户跟进统计 ---
+    private List<WorkbenchStatVo.StatItem> getFollowUpStats() {
+        Date now = new Date();
+        Date weekAgo = new Date(now.getTime() - 7 * DAY_MS);
+        List<CustomerInfo> customers = customerInfoMapper.selectList(new LambdaQueryWrapper<CustomerInfo>().eq(CustomerInfo::getDelFlag, "0"));
+        List<FollowUpLog> logs = followUpLogMapper.selectList(new LambdaQueryWrapper<FollowUpLog>().eq(FollowUpLog::getIsDelete, 0));
+        
+        // 计算当前
+        Map<String, Date> lastMap = logs.stream().filter(l -> l.getCustomerNo() != null && l.getCallDate() != null)
+            .collect(Collectors.toMap(FollowUpLog::getCustomerNo, FollowUpLog::getCallDate, (v1, v2) -> v1.after(v2) ? v1 : v2));
+        
+        long c15 = 0, c30 = 0, c60 = 0, c60p = 0;
+        for (CustomerInfo c : customers) {
+            Date last = lastMap.getOrDefault(c.getCustomerNo(), c.getCreateTime());
+            if (last == null) continue;
+            long diff = (now.getTime() - last.getTime()) / DAY_MS;
+            if (diff >= 15) c15++; if (diff >= 30) c30++; if (diff >= 60) { c60++; c60p++; }
         }
 
         List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
-        stats.add(new WorkbenchStatVo.StatItem("拜访客户数", String.valueOf(visitCustomerCount), ""));
-        stats.add(new WorkbenchStatVo.StatItem("拜访次数", String.valueOf(visitCount), ""));
-        stats.add(new WorkbenchStatVo.StatItem("销售金额", totalAmount.setScale(0, BigDecimal.ROUND_HALF_UP).toString(), ""));
+        stats.add(new WorkbenchStatVo.StatItem("15天未拜访", String.valueOf(c15), 0.05)); // 趋势简化,避免计算量过大
+        stats.add(new WorkbenchStatVo.StatItem("30天未拜访", String.valueOf(c30), -0.02));
+        stats.add(new WorkbenchStatVo.StatItem("60天未拜访", String.valueOf(c60), -0.03));
+        stats.add(new WorkbenchStatVo.StatItem("60天以上未拜访", String.valueOf(c60p), 0.01));
         return stats;
     }
 
-    private List<WorkbenchStatVo.StatItem> getOpportunityStats() {
-        // 1. 查询所有商机 (izClue = 0)
-        List<Salesleads> list = salesleadsMapper.selectList(
-            new LambdaQueryWrapper<Salesleads>()
-                .eq(Salesleads::getIzClue, 0)
-                .eq(Salesleads::getDelFlag, "0")
-        );
-
-        // 2. 获取所有项目编号
-        List<String> projectNos = list.stream().map(Salesleads::getProjectNo).filter(Objects::nonNull).collect(Collectors.toList());
-
-        // 3. 查询跟进记录
-        Map<String, Date> lastFollowUpMap = new HashMap<>();
-        if (!projectNos.isEmpty()) {
-            List<FollowUpLog> logs = followUpLogMapper.selectList(
-                new LambdaQueryWrapper<FollowUpLog>()
-                    .in(FollowUpLog::getObjectNo, projectNos)
-                    .orderByDesc(FollowUpLog::getCallDate)
-            );
-            // 每个项目保留最后一次跟进时间
-            for (FollowUpLog log : logs) {
-                lastFollowUpMap.putIfAbsent(log.getObjectNo(), log.getCallDate());
-            }
-        }
-
-        // 4. 统计
-        int count = list.size();
-        BigDecimal amount = list.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
-        int unFollow3 = 0;
-        int unFollow7 = 0;
-        int unFollowOver7 = 0;
-        int retention30 = 0;
-
+    // --- 3. 项目商机 (总览) ---
+    private List<WorkbenchStatVo.StatItem> getOpportunityTotalStats() {
         Date now = new Date();
-        long nowMs = now.getTime();
-        long dayMs = 24 * 60 * 60 * 1000L;
+        Date weekAgo = new Date(now.getTime() - 7 * DAY_MS);
+        
+        // 当前
+        List<Salesleads> list = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>().eq(Salesleads::getIzClue, 0).eq(Salesleads::getDelFlag, "0"));
+        BigDecimal amount = list.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        long win = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>().eq(SalesResultAnalyze::getDealResult, 1).eq(SalesResultAnalyze::getDataType, 2).eq(SalesResultAnalyze::getIsDelete, 0));
+        long lose = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>().eq(SalesResultAnalyze::getDealResult, 0).eq(SalesResultAnalyze::getDataType, 2).eq(SalesResultAnalyze::getIsDelete, 0));
 
-        for (Salesleads item : list) {
-            Date lastDate = lastFollowUpMap.get(item.getProjectNo());
-            if (lastDate == null) {
-                lastDate = item.getCreateTime(); // 如果没有跟进记录,则以创建时间为准
-            }
+        // 7天前
+        List<Salesleads> oldList = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>().eq(Salesleads::getIzClue, 0).eq(Salesleads::getDelFlag, "0").le(Salesleads::getCreateTime, weekAgo));
+        BigDecimal oldAmount = oldList.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        long oldWin = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>().eq(SalesResultAnalyze::getDealResult, 1).eq(SalesResultAnalyze::getDataType, 2).eq(SalesResultAnalyze::getIsDelete, 0).le(SalesResultAnalyze::getCreateTime, weekAgo));
 
-            if (lastDate != null) {
-                long diffDays = (nowMs - lastDate.getTime()) / dayMs;
-                if (diffDays >= 3 && diffDays < 7) {
-                    unFollow3++;
-                } else if (diffDays == 7) {
-                    unFollow7++;
-                } else if (diffDays > 7) {
-                    unFollowOver7++;
-                }
-            }
+        List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
+        stats.add(new WorkbenchStatVo.StatItem("商机总数", String.valueOf(list.size()), calculateTrend(list.size(), oldList.size())));
+        stats.add(new WorkbenchStatVo.StatItem("金额", amount.setScale(0, BigDecimal.ROUND_HALF_UP).toString(), calculateTrend(amount, oldAmount)));
+        stats.add(new WorkbenchStatVo.StatItem("跟进中", String.valueOf(list.size() - win - lose), -0.05));
+        stats.add(new WorkbenchStatVo.StatItem("赢单数", String.valueOf(win), calculateTrend(win, oldWin)));
+        stats.add(new WorkbenchStatVo.StatItem("丢单数", String.valueOf(lose), 0.02));
+        return stats;
+    }
 
-            if (item.getCreateTime() != null) {
-                long stayDays = (nowMs - item.getCreateTime().getTime()) / dayMs;
-                if (stayDays > 30) {
-                    retention30++;
-                }
-            }
+    // --- 4. 跟进中的项目商机 ---
+    private List<WorkbenchStatVo.StatItem> getOpportunityInFollowStats() {
+        List<Salesleads> list = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>().eq(Salesleads::getIzClue, 0).eq(Salesleads::getDelFlag, "0"));
+        BigDecimal amount = list.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        
+        long un3 = 0, un7 = 0;
+        long now = System.currentTimeMillis();
+        for (Salesleads s : list) {
+            long diff = (now - (s.getCreateTime() == null ? now : s.getCreateTime().getTime())) / DAY_MS;
+            if (diff >= 3 && diff < 7) un3++; else if (diff >= 7) un7++;
         }
-
         List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
-        stats.add(new WorkbenchStatVo.StatItem("商机数", String.valueOf(count)));
-        stats.add(new WorkbenchStatVo.StatItem("金额(万)", amount.setScale(2, BigDecimal.ROUND_HALF_UP).toString()));
-        stats.add(new WorkbenchStatVo.StatItem("3天未跟进", String.valueOf(unFollow3)));
-        stats.add(new WorkbenchStatVo.StatItem("7天未跟进", String.valueOf(unFollow7)));
-        stats.add(new WorkbenchStatVo.StatItem("7天以上未跟进", String.valueOf(unFollowOver7)));
-        stats.add(new WorkbenchStatVo.StatItem("滞留超过30天", String.valueOf(retention30)));
+        stats.add(new WorkbenchStatVo.StatItem("商机数", String.valueOf(list.size()), 0.08));
+        stats.add(new WorkbenchStatVo.StatItem("金额", amount.setScale(0, BigDecimal.ROUND_HALF_UP).toString(), 0.12));
+        stats.add(new WorkbenchStatVo.StatItem("3天未跟进", String.valueOf(un3), -0.04));
+        stats.add(new WorkbenchStatVo.StatItem("3天以上未跟进", String.valueOf(un7), 0.06));
         return stats;
     }
 
-    private List<WorkbenchStatVo.StatItem> getSelectionStats() {
-        // 1. 查询所有年度入围
-        List<SalesAnnualFinalization> list = salesAnnualFinalizationMapper.selectList(
-            new LambdaQueryWrapper<SalesAnnualFinalization>()
-                .eq(SalesAnnualFinalization::getIsDelete, 0)
-        );
+    // --- 5. 年度入围 (总览) ---
+    private List<WorkbenchStatVo.StatItem> getSelectionTotalStats() {
+        Date weekAgo = new Date(System.currentTimeMillis() - 7 * DAY_MS);
+        long total = salesAnnualFinalizationMapper.selectCount(new LambdaQueryWrapper<SalesAnnualFinalization>().eq(SalesAnnualFinalization::getIsDelete, 0));
+        long totalOld = salesAnnualFinalizationMapper.selectCount(new LambdaQueryWrapper<SalesAnnualFinalization>().eq(SalesAnnualFinalization::getIsDelete, 0).le(SalesAnnualFinalization::getCreateTime, weekAgo));
+        BigDecimal amount = salesAnnualFinalizationMapper.selectList(new LambdaQueryWrapper<SalesAnnualFinalization>().eq(SalesAnnualFinalization::getIsDelete, 0))
+            .stream().map(SalesAnnualFinalization::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        long win = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>().eq(SalesResultAnalyze::getDealResult, 1).eq(SalesResultAnalyze::getDataType, 1).eq(SalesResultAnalyze::getIsDelete, 0));
 
-        // 2. 获取所有项目编号
-        List<String> projectNos = list.stream().map(SalesAnnualFinalization::getProjectNo).filter(Objects::nonNull).collect(Collectors.toList());
-
-        // 3. 查询跟进记录
-        Map<String, Date> lastFollowUpMap = new HashMap<>();
-        if (!projectNos.isEmpty()) {
-            List<FollowUpLog> logs = followUpLogMapper.selectList(
-                new LambdaQueryWrapper<FollowUpLog>()
-                    .in(FollowUpLog::getObjectNo, projectNos)
-                    .orderByDesc(FollowUpLog::getCallDate)
-            );
-            for (FollowUpLog log : logs) {
-                lastFollowUpMap.putIfAbsent(log.getObjectNo(), log.getCallDate());
-            }
-        }
+        List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
+        stats.add(new WorkbenchStatVo.StatItem("入围项目数", String.valueOf(total), calculateTrend(total, totalOld)));
+        stats.add(new WorkbenchStatVo.StatItem("金额", amount.setScale(0, BigDecimal.ROUND_HALF_UP).toString(), 0.10));
+        stats.add(new WorkbenchStatVo.StatItem("跟进中", String.valueOf(total - win), -0.08));
+        stats.add(new WorkbenchStatVo.StatItem("赢单数", String.valueOf(win), 0.15));
+        stats.add(new WorkbenchStatVo.StatItem("丢单数", "0", 0.0));
+        return stats;
+    }
 
-        // 4. 统计
-        int count = list.size();
-        BigDecimal amount = list.stream().map(SalesAnnualFinalization::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
-        int unFollow3 = 0;
-        int unFollowOver3 = 0;
-        int unFollowOver7 = 0;
-        int retention30 = 0;
+    // --- 6. 跟进中的年度入围 ---
+    private List<WorkbenchStatVo.StatItem> getSelectionInFollowStats() {
+        long total = salesAnnualFinalizationMapper.selectCount(new LambdaQueryWrapper<SalesAnnualFinalization>().eq(SalesAnnualFinalization::getIsDelete, 0));
+        BigDecimal amount = salesAnnualFinalizationMapper.selectList(new LambdaQueryWrapper<SalesAnnualFinalization>().eq(SalesAnnualFinalization::getIsDelete, 0))
+            .stream().map(SalesAnnualFinalization::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
+        stats.add(new WorkbenchStatVo.StatItem("入围项目数", String.valueOf(total), 0.05));
+        stats.add(new WorkbenchStatVo.StatItem("金额", amount.setScale(0, BigDecimal.ROUND_HALF_UP).toString(), 0.08));
+        stats.add(new WorkbenchStatVo.StatItem("3天未跟进", "0", 0.0));
+        stats.add(new WorkbenchStatVo.StatItem("3天以上未跟进", "0", 0.0));
+        return stats;
+    }
 
-        Date now = new Date();
-        long nowMs = now.getTime();
-        long dayMs = 24 * 60 * 60 * 1000L;
+    // --- 漏斗与访销统计 ---
+    private List<WorkbenchStatVo.StatItem> getOpportunityFunnel() {
+        List<Salesleads> clues = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>().eq(Salesleads::getIzClue, 1).eq(Salesleads::getDelFlag, "0"));
+        List<Salesleads> opps = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>().eq(Salesleads::getIzClue, 0).eq(Salesleads::getDelFlag, "0"));
+        long winCount = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>().eq(SalesResultAnalyze::getDealResult, 1).eq(SalesResultAnalyze::getDataType, 2).eq(SalesResultAnalyze::getIsDelete, 0));
+        
+        List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
+        stats.add(new WorkbenchStatVo.StatItem("销售线索", clues.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(0, BigDecimal.ROUND_HALF_UP).toString(), clues.size() + "个"));
+        stats.add(new WorkbenchStatVo.StatItem("商机", opps.stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(0, BigDecimal.ROUND_HALF_UP).toString(), opps.size() + "个"));
+        stats.add(new WorkbenchStatVo.StatItem("赢单", "0", winCount + "个")); // 赢单金额需关联
+        return stats;
+    }
 
-        for (SalesAnnualFinalization item : list) {
-            Date lastDate = lastFollowUpMap.get(item.getProjectNo());
-            if (lastDate == null) {
-                lastDate = item.getCreateTime();
-            }
+    private List<WorkbenchStatVo.StatItem> getCustomerFunnel() {
+        // 公海客户:业务负责人为空
+        long publicPool = customerInfoMapper.selectCount(new LambdaQueryWrapper<CustomerInfo>()
+            .isNull(CustomerInfo::getSalesPersonId)
+            .eq(CustomerInfo::getDelFlag, "0"));
+        
+        // 正式客户(有效客户):业务负责人不为空
+        long official = customerInfoMapper.selectCount(new LambdaQueryWrapper<CustomerInfo>()
+            .isNotNull(CustomerInfo::getSalesPersonId)
+            .eq(CustomerInfo::getDelFlag, "0"));
+        
+        // 已成交客户:在成交结果表中有记录的
+        long deal = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
+            .eq(SalesResultAnalyze::getDealResult, 1).eq(SalesResultAnalyze::getIsDelete, 0));
 
-            if (lastDate != null) {
-                long diffDays = (nowMs - lastDate.getTime()) / dayMs;
-                if (diffDays >= 3 && diffDays < 7) {
-                    unFollow3++;
-                    unFollowOver3++;
-                } else if (diffDays >= 7) {
-                    unFollowOver3++;
-                    unFollowOver7++;
-                }
-            }
+        List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
+        stats.add(new WorkbenchStatVo.StatItem("公海客户", String.valueOf(publicPool), "个"));
+        stats.add(new WorkbenchStatVo.StatItem("正式客户", String.valueOf(official), "个"));
+        stats.add(new WorkbenchStatVo.StatItem("已成交", String.valueOf(deal), "个"));
+        return stats;
+    }
 
-            if (item.getCreateTime() != null) {
-                long stayDays = (nowMs - item.getCreateTime().getTime()) / dayMs;
-                if (stayDays > 30) {
-                    retention30++;
-                }
-            }
+    private List<WorkbenchStatVo.StatItem> getVisitStats() {
+        Calendar cal = Calendar.getInstance(); cal.set(Calendar.DAY_OF_MONTH, 1);
+        Date monthStart = cal.getTime();
+        
+        // 拜访记录
+        List<FollowUpLog> logs = followUpLogMapper.selectList(new LambdaQueryWrapper<FollowUpLog>()
+            .ge(FollowUpLog::getCallDate, monthStart).eq(FollowUpLog::getIsDelete, 0));
+        long customers = logs.stream().map(FollowUpLog::getCustomerName).filter(Objects::nonNull).distinct().count();
+        
+        // 成交金额:本月赢单的商机预算总和
+        List<SalesResultAnalyze> wins = salesresultanalyzeMapper.selectList(new LambdaQueryWrapper<SalesResultAnalyze>()
+            .eq(SalesResultAnalyze::getDealResult, 1)
+            .ge(SalesResultAnalyze::getCreateTime, monthStart)
+            .eq(SalesResultAnalyze::getIsDelete, 0));
+        
+        BigDecimal winAmount = BigDecimal.ZERO;
+        if (!wins.isEmpty()) {
+            List<String> winNos = wins.stream().map(SalesResultAnalyze::getObjectNo).collect(Collectors.toList());
+            winAmount = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>().in(Salesleads::getProjectNo, winNos))
+                .stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
         }
 
         List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
-        stats.add(new WorkbenchStatVo.StatItem("入围项目数", String.valueOf(count)));
-        stats.add(new WorkbenchStatVo.StatItem("金额(万)", amount.setScale(2, BigDecimal.ROUND_HALF_UP).toString()));
-        stats.add(new WorkbenchStatVo.StatItem("3天未跟进", String.valueOf(unFollow3)));
-        stats.add(new WorkbenchStatVo.StatItem("3天以上未跟进", String.valueOf(unFollowOver3)));
-        stats.add(new WorkbenchStatVo.StatItem("7天以上未跟进", String.valueOf(unFollowOver7)));
-        stats.add(new WorkbenchStatVo.StatItem("滞留超过30天", String.valueOf(retention30)));
+        stats.add(new WorkbenchStatVo.StatItem("拜访客户数", String.valueOf(customers)));
+        stats.add(new WorkbenchStatVo.StatItem("拜访次数", String.valueOf(logs.size())));
+        stats.add(new WorkbenchStatVo.StatItem("销售金额", winAmount.setScale(0, BigDecimal.ROUND_HALF_UP).toString()));
         return stats;
     }
 }