Quellcode durchsuchen

Merge branch 'tys' into hurx05080913

# Conflicts:
#	ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerContactVo.java
#	ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerContactServiceImpl.java
#	ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerInfoServiceImpl.java
hurx vor 3 Wochen
Ursprung
Commit
74adba0a15
28 geänderte Dateien mit 1025 neuen und 351 gelöschten Zeilen
  1. 8 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/CustomerInfoController.java
  2. 4 5
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/OperationLogController.java
  3. 20 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/constant/CustomerConstants.java
  4. 28 63
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/CustomerCare.java
  5. 113 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/CustomerContactInfo.java
  6. 0 5
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/CustomerCareBo.java
  7. 75 1
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/CustomerContactBo.java
  8. 2 12
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerCareVo.java
  9. 70 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerContactVo.java
  10. 30 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerInfoVo.java
  11. 1 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerSalesInfoVo.java
  12. 6 3
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/OperationLogVo.java
  13. 3 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/SalesleadsVo.java
  14. 27 18
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/WorkbenchStatVo.java
  15. 16 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/mapper/CustomerContactInfoMapper.java
  16. 7 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/IOperationLogService.java
  17. 42 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CrmVisitPlanServiceImpl.java
  18. 55 1
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerCareServiceImpl.java
  19. 53 4
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerContactServiceImpl.java
  20. 174 22
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerInfoServiceImpl.java
  21. 16 1
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/OperationLogServiceImpl.java
  22. 30 8
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SalesAnnualFinalizationServiceImpl.java
  23. 26 1
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SalesleadsServiceImpl.java
  24. 202 199
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/WorkbenchServiceImpl.java
  25. 3 1
      ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/SalesleadsMapper.xml
  26. 6 3
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/ComCustomerLevelVo.java
  27. 6 1
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/ComCustomerTypeVo.java
  28. 2 3
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteComCustomerLevelServiceImpl.java

+ 8 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/CustomerInfoController.java

@@ -282,4 +282,12 @@ public class CustomerInfoController extends BaseController {
     public R<List<RemoteComCustomerLevelVo>> levelOptionList() {
         return R.ok(remoteComCustomerLevelService.selectCustomerLevelList());
     }
+
+    /**
+     * 获取客户下拉列表
+     */
+    @GetMapping("/optionList")
+    public R<List<CustomerInfoVo>> optionList(CustomerInfoBo bo) {
+        return R.ok(customerInfoService.queryList(bo));
+    }
 }

+ 4 - 5
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/OperationLogController.java

@@ -1,7 +1,8 @@
 package org.dromara.customer.controller;
 
 import lombok.RequiredArgsConstructor;
-import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.customer.domain.bo.OperationLogBo;
 import org.dromara.customer.domain.vo.OperationLogVo;
 import org.dromara.customer.service.IOperationLogService;
@@ -9,8 +10,6 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.List;
-
 /**
  * 操作日志Controller
  * @author tys
@@ -26,7 +25,7 @@ public class OperationLogController {
      * 查询操作日志列表
      */
     @GetMapping("/list")
-    public R<List<OperationLogVo>> list(OperationLogBo bo) {
-        return R.ok(operationLogService.queryList(bo));
+    public TableDataInfo<OperationLogVo> list(OperationLogBo bo, PageQuery pageQuery) {
+        return operationLogService.queryPageList(bo, pageQuery);
     }
 }

+ 20 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/constant/CustomerConstants.java

@@ -26,6 +26,26 @@ public interface CustomerConstants {
      */
     Integer DATA_TYPE_LEADS = 1;
 
+    /**
+     * 数据类型:年度入围(项目)
+     */
+    Integer DATA_TYPE_PROJECT_SELECTION = 2;
+
+    /**
+     * 数据类型:年度入围(平台)
+     */
+    Integer DATA_TYPE_PLATFORM_SELECTION = 3;
+
+    /**
+     * 数据类型:客户关怀
+     */
+    Integer DATA_TYPE_CARE = 10;
+
+    /**
+     * 模块:客户关怀 (Integer 编码)
+     */
+    Integer MODULE_CARE = 10;
+
     /**
      * 操作类型:新增
      */

+ 28 - 63
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/CustomerCare.java

@@ -26,169 +26,134 @@ public class CustomerCare extends TenantEntity {
     /**
      * 主键ID
      */
-    @TableId(value = "Id")
+    @TableId
     private Long id;
 
     /**
      * 收据/单据编号
      */
-    @TableField("ReceiptId")
+    @TableField("receipt_id")
     private String receiptId;
 
     /**
      * 客户ID
      */
-    @TableField("CustomerId")
+    @TableField("customer_id")
     private String customerId;
 
     /**
      * 客户名称
      */
-    @TableField("CustomerName")
+    @TableField("customer_name")
     private String customerName;
 
     /**
      * 行业
      */
-    @TableField("Profession")
+    @TableField("profession")
     private String profession;
 
     /**
      * 部门
      */
-    @TableField("Department")
+    @TableField("department")
     private String department;
 
     /**
-     * 销售
+     * 业务
      */
-    @TableField("Salesman")
+    @TableField("salesman")
     private String salesman;
 
     /**
-     * 人员编号
+     * 联系人id
      */
-    @TableField("PersonnelNumber")
+    @TableField("personnel_number")
     private String personnelNumber;
 
     /**
      * 联系人
      */
-    @TableField("ContactPerson")
+    @TableField("contact_person")
     private String contactPerson;
 
     /**
      * 电话
      */
-    @TableField("Phone")
+    @TableField("phone")
     private String phone;
 
     /**
      * 固定电话
      */
-    @TableField("Telephone")
+    @TableField("telephone")
     private String telephone;
 
     /**
      * 关注类型
      */
-    @TableField("ConcernType")
+    @TableField("concern_type")
     private String concernType;
 
     /**
      * 金额
      */
-    @TableField("Amount")
+    @TableField("amount")
     private BigDecimal amount;
 
     /**
      * 需求日期
      */
-    @TableField("RequirementDate")
+    @TableField("requirement_date")
     private Date requirementDate;
 
     /**
      * 文件编号
      */
-    @TableField("FileNo")
+    @TableField("file_no")
     private String fileNo;
 
     /**
      * 礼品描述
      */
-    @TableField("GiftDesc")
+    @TableField("gift_desc")
     private String giftDesc;
 
     /**
      * 关注论据
      */
-    @TableField("ConcernArgument")
+    @TableField("concern_argument")
     private String concernArgument;
 
     /**
-     * 审核状态(0-待审核,1-已通过,2-已驳回)
+     * 审核状态
      */
-    @TableField("AuditStatus")
+    @TableField("audit_status")
     private Integer auditStatus;
 
     /**
      * 审核人ID
      */
-    @TableField("AuditPeople")
+    @TableField("audit_people")
     private Long auditPeople;
 
     /**
      * 审核日期
      */
-    @TableField("AuditDate")
+    @TableField("audit_date")
     private Date auditDate;
 
     /**
-     * 创建组织ID(对应数据库 CreateOrgId,由 createDept 填充)
+     * 是否删除 (0-未删除, 1-已删除)
      */
-    @TableField(value = "CreateOrgId", fill = FieldFill.INSERT)
-    private Long createOrgId;
+    @TableLogic(value = "0", delval = "1")
+    @TableField("is_delete")
+    private Boolean isDelete;
 
     /**
-     * 创建部门 (表无此列,排除;setter 同步到 createOrgId)
+     * 排除基类中的创建部门字段(SQL中不存在)
      */
     @TableField(exist = false)
     private Long createDept;
 
-    public void setCreateDept(Long createDept) {
-        this.createDept = createDept;
-        this.createOrgId = createDept;
-    }
-
-    /**
-     * 创建者 (覆盖 BaseEntity)
-     */
-    @TableField(value = "CreateUserId", fill = FieldFill.INSERT)
-    private Long createBy;
-
-    /**
-     * 创建时间 (覆盖 BaseEntity)
-     */
-    @TableField(value = "CreateTime", fill = FieldFill.INSERT)
-    private Date createTime;
-
-    /**
-     * 更新者 (覆盖 BaseEntity)
-     */
-    @TableField(value = "UpdateUserId", fill = FieldFill.INSERT_UPDATE)
-    private Long updateBy;
-
-    /**
-     * 更新时间 (覆盖 BaseEntity)
-     */
-    @TableField(value = "UpdateTime", fill = FieldFill.INSERT_UPDATE)
-    private Date updateTime;
-
-    /**
-     * 是否删除 (0-未删除, 1-已删除)
-     */
-    @TableLogic(value = "0", delval = "1")
-    @TableField("IsDelete")
-    private Boolean isDelete = false;
-
 }

+ 113 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/CustomerContactInfo.java

@@ -0,0 +1,113 @@
+package org.dromara.customer.domain;
+
+import org.dromara.customer.domain.vo.CustomerContactVo;
+import org.dromara.common.tenant.core.TenantEntity;
+import com.baomidou.mybatisplus.annotation.*;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 客户联系人扩展/生活信息对象 customer_contact_info
+ *
+ * @author LionLi
+ * @date 2026-05-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("customer_contact_info")
+@AutoMapper(target = CustomerContactVo.class)
+public class CustomerContactInfo extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 关联联系人ID
+     */
+    private Long contactId;
+
+    /**
+     * 联系人类型 (1公司职员 2关系资源人)
+     */
+    private String type;
+
+    /**
+     * 年龄
+     */
+    private Integer age;
+
+    /**
+     * 籍贯
+     */
+    private String nativePlace;
+
+    /**
+     * 在职状态 (1在职 2离职)
+     */
+    private String jobStatus;
+
+    /**
+     * 工作内容
+     */
+    private String jobContent;
+
+    /**
+     * 家庭情况
+     */
+    private String familyStatus;
+
+    /**
+     * 爱好
+     */
+    private String hobby;
+
+    /**
+     * 性格特征
+     */
+    private String characterTrait;
+
+    /**
+     * 是否抽烟 (1是 0否)
+     */
+    private String isSmoke;
+
+    /**
+     * 是否喝酒 (1是 0否)
+     */
+    private String isDrink;
+
+    /**
+     * 家庭地址(省)ID
+     */
+    private Long homeProvinceId;
+
+    /**
+     * 家庭地址(市)ID
+     */
+    private Long homeCityId;
+    
+    /**
+     * 家庭地址(区)ID
+     */
+    private Long homeAreaId;
+
+    /**
+     * 家庭详细地址
+     */
+    private String homeAddressDetail;
+    
+    /**
+     * 平台标识
+     */
+    private String platformCode;
+
+}

+ 0 - 5
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/CustomerCareBo.java

@@ -120,11 +120,6 @@ public class CustomerCareBo extends BaseEntity {
      */
     private Date auditDate;
 
-    /**
-     * 创建组织ID
-     */
-    private Long createOrgId;
-
     /**
      * 平台编码
      */

+ 75 - 1
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/CustomerContactBo.java

@@ -5,9 +5,11 @@ import org.dromara.common.mybatis.core.domain.BaseEntity;
 import org.dromara.common.core.validate.AddGroup;
 import org.dromara.common.core.validate.EditGroup;
 import io.github.linpeilie.annotations.AutoMapper;
+import io.github.linpeilie.annotations.AutoMappers;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import jakarta.validation.constraints.*;
+import org.dromara.customer.domain.CustomerContactInfo;
 
 import java.util.Date;
 
@@ -19,7 +21,10 @@ import java.util.Date;
  */
 @Data
 @EqualsAndHashCode(callSuper = true)
-@AutoMapper(target = CustomerContact.class, reverseConvertGenerate = false)
+@AutoMappers({
+    @AutoMapper(target = CustomerContact.class, reverseConvertGenerate = false),
+    @AutoMapper(target = CustomerContactInfo.class, reverseConvertGenerate = false)
+})
 public class CustomerContactBo extends BaseEntity {
 
     /**
@@ -134,5 +139,74 @@ public class CustomerContactBo extends BaseEntity {
      */
     private String customerNo;
 
+    /**
+     * 联系人类型 (1公司职员 2关系资源人)
+     */
+    private String type;
+
+    /**
+     * 年龄
+     */
+    private Integer age;
+
+    /**
+     * 籍贯
+     */
+    private String nativePlace;
+
+    /**
+     * 在职状态 (1在职 2离职)
+     */
+    private String jobStatus;
+
+    /**
+     * 工作内容
+     */
+    private String jobContent;
+
+    /**
+     * 家庭情况
+     */
+    private String familyStatus;
+
+    /**
+     * 爱好
+     */
+    private String hobby;
+
+    /**
+     * 性格特征
+     */
+    private String characterTrait;
+
+    /**
+     * 是否抽烟 (1是 0否)
+     */
+    private String isSmoke;
+
+    /**
+     * 是否喝酒 (1是 0否)
+     */
+    private String isDrink;
+
+    /**
+     * 家庭地址(省)ID
+     */
+    private Long homeProvinceId;
+
+    /**
+     * 家庭地址(市)ID
+     */
+    private Long homeCityId;
+
+    /**
+     * 家庭地址(区)ID
+     */
+    private Long homeAreaId;
+
+    /**
+     * 家庭详细地址
+     */
+    private String homeAddressDetail;
 
 }

+ 2 - 12
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerCareVo.java

@@ -122,20 +122,10 @@ public class CustomerCareVo implements Serializable {
      */
     private Date auditDate;
 
-    /**
-     * 创建组织ID
-     */
-    private Long createOrgId;
-
-    /**
-     * 平台编码
-     */
-    private String platformCode;
-
     /**
      * 创建者
      */
-    private Long createUserId;
+    private Long createBy;
 
     /**
      * 创建时间
@@ -145,7 +135,7 @@ public class CustomerCareVo implements Serializable {
     /**
      * 更新者
      */
-    private Long updateUserId;
+    private Long updateBy;
 
     /**
      * 更新时间

+ 70 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/CustomerContactVo.java

@@ -159,4 +159,74 @@ public class CustomerContactVo implements Serializable {
     private Date createTime;
 
     private String delFlag;
+
+    /**
+     * 联系人类型 (1公司职员 2关系资源人)
+     */
+    private String type;
+
+    /**
+     * 年龄
+     */
+    private Integer age;
+
+    /**
+     * 籍贯
+     */
+    private String nativePlace;
+
+    /**
+     * 在职状态 (1在职 2离职)
+     */
+    private String jobStatus;
+
+    /**
+     * 工作内容
+     */
+    private String jobContent;
+
+    /**
+     * 家庭情况
+     */
+    private String familyStatus;
+
+    /**
+     * 爱好
+     */
+    private String hobby;
+
+    /**
+     * 性格特征
+     */
+    private String characterTrait;
+
+    /**
+     * 是否抽烟 (1是 0否)
+     */
+    private String isSmoke;
+
+    /**
+     * 是否喝酒 (1是 0否)
+     */
+    private String isDrink;
+
+    /**
+     * 家庭地址(省)ID
+     */
+    private Long homeProvinceId;
+
+    /**
+     * 家庭地址(市)ID
+     */
+    private Long homeCityId;
+
+    /**
+     * 家庭地址(区)ID
+     */
+    private Long homeAreaId;
+
+    /**
+     * 家庭详细地址
+     */
+    private String homeAddressDetail;
 }

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

@@ -248,6 +248,8 @@ public class CustomerInfoVo implements Serializable {
 
     private String belongingDepartmentName;
 
+    private String deptName;
+
     /*部门额度*/
     private BigDecimal deptCredit;
 
@@ -261,10 +263,38 @@ public class CustomerInfoVo implements Serializable {
 
     /** 行业名称 */
     private String industryName;
+    /** 省份名称 */
+    private String regProvincialName;
+    /** 城市名称 */
+    private String regCityName;
     /** 企业类型名称 */
     private String enterpriseTypeName;
     /** 客户等级名称 */
     private String customerLevelName;
     /** 合作状态名称 */
     private String cooperationName;
+    /** 客户来源名称 */
+    private String customerSourceName;
+    /** 开票类型 */
+    private String invoiceType;
+    /** 开票类型名称 */
+    private String invoiceTypeName;
+
+    /** 创建者 */
+    private String createBy;
+
+    /** 创建时间 */
+    private Date createTime;
+
+    /** 更新者 */
+    private String updateBy;
+
+    /** 更新时间 */
+    private Date updateTime;
+
+    /** 创建者姓名 */
+    private String createByName;
+
+    /** 更新者姓名 */
+    private String updateByName;
 }

+ 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;
 
 }

+ 6 - 3
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/OperationLogVo.java

@@ -1,9 +1,8 @@
 package org.dromara.customer.domain.vo;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import io.github.linpeilie.annotations.AutoMapper;
 import lombok.Data;
-import org.dromara.common.translation.annotation.Translation;
-import org.dromara.common.translation.constant.TransConstant;
 import org.dromara.customer.domain.OperationLog;
 
 import java.io.Serial;
@@ -41,11 +40,13 @@ public class OperationLogVo implements Serializable {
     /**
      * 操作人
      */
+    @JsonProperty("operName")
     private String operator;
 
     /**
      * 操作类型
      */
+    @JsonProperty("operAction")
     private Integer actionType;
 
     /**
@@ -61,6 +62,7 @@ public class OperationLogVo implements Serializable {
     /**
      * 操作详情
      */
+    @JsonProperty("operContent")
     private String actionDetails;
 
     /**
@@ -71,6 +73,7 @@ public class OperationLogVo implements Serializable {
     /**
      * 创建时间
      */
+    @JsonProperty("operTime")
     private Date createTime;
 
     /**
@@ -81,7 +84,7 @@ public class OperationLogVo implements Serializable {
     /**
      * 创建者姓名
      */
-    @Translation(type = TransConstant.USER_ID_TO_NAME, mapper = "createBy")
     private String createByName;
 
 }
+

+ 3 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/vo/SalesleadsVo.java

@@ -158,6 +158,9 @@ public class SalesleadsVo implements Serializable {
     @Schema(description = "创建时间")
     private Date createTime;
 
+    @Schema(description = "成交结果")
+    private String dealResult;
+
     @Schema(description = "更新时间")
     private Date updateTime;
 }

+ 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;

+ 16 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/mapper/CustomerContactInfoMapper.java

@@ -0,0 +1,16 @@
+package org.dromara.customer.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.customer.domain.CustomerContactInfo;
+
+/**
+ * 客户联系人扩展/生活信息Mapper接口
+ *
+ * @author LionLi
+ * @date 2026-05-04
+ */
+@Mapper
+public interface CustomerContactInfoMapper extends BaseMapperPlus<CustomerContactInfo, CustomerContactInfo> {
+
+}

+ 7 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/IOperationLogService.java

@@ -1,5 +1,7 @@
 package org.dromara.customer.service;
 
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.customer.domain.bo.OperationLogBo;
 import org.dromara.customer.domain.vo.OperationLogVo;
 
@@ -12,6 +14,11 @@ import java.util.List;
  */
 public interface IOperationLogService {
 
+    /**
+     * 分页查询操作日志列表
+     */
+    TableDataInfo<OperationLogVo> queryPageList(OperationLogBo bo, PageQuery pageQuery);
+
     /**
      * 查询操作日志列表
      */

+ 42 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CrmVisitPlanServiceImpl.java

@@ -13,11 +13,13 @@ import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.customer.domain.VisitPlan;
 import org.dromara.customer.domain.bo.CrmVisitPlanBo;
+import org.dromara.customer.domain.bo.TeamMemberBo;
 import org.dromara.customer.domain.bo.VisitRoutineBo;
 import org.dromara.customer.domain.vo.CrmVisitPlanVo;
 import org.dromara.customer.mapper.CrmVisitPlanMapper;
 import org.dromara.customer.service.ICrmVisitPlanService;
 import org.dromara.customer.service.ICrmVisitRoutineService;
+import org.dromara.customer.service.ITeamMemberService;
 import org.dromara.customer.service.IOperationLogService;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -35,6 +37,7 @@ public class CrmVisitPlanServiceImpl implements ICrmVisitPlanService {
     private final CrmVisitPlanMapper baseMapper;
     private final ICrmVisitRoutineService crmVisitRoutineService;
     private final IOperationLogService operateLogService;
+    private final ITeamMemberService teamMemberService;
 
     @Override
     public CrmVisitPlanVo queryById(Long id) {
@@ -85,6 +88,18 @@ public class CrmVisitPlanServiceImpl implements ICrmVisitPlanService {
             operateLogService.asyncRecordLog(add.getId(), "新建计划", "创建了拜访计划及任务");
             bo.setId(add.getId());
             insertDetails(bo);
+
+            // 自动将拜访人添加为该计划的团队成员 (dataType=4)
+            if (bo.getVisitorId() != null) {
+                TeamMemberBo planMember = new TeamMemberBo();
+                planMember.setObjectNo(bo.getPlanNo());
+                planMember.setUserNo(bo.getVisitorId());
+                planMember.setRealName(bo.getVisitorName());
+                planMember.setRoleCode("1"); // 计划负责人
+                planMember.setUpdateAccredit(1); // 拥有修改权限
+                planMember.setDataType(4); // 拜访计划类型
+                teamMemberService.insertOrUpdateMember(planMember);
+            }
         }
         return flag;
     }
@@ -115,6 +130,33 @@ public class CrmVisitPlanServiceImpl implements ICrmVisitPlanService {
                 detail.setCallPeopleNo(bo.getVisitorId());
                 detail.setCallPeopleName(bo.getVisitorName());
                 crmVisitRoutineService.insertByBo(detail);
+
+                // 自动添加团队成员
+                if (bo.getVisitorId() != null) {
+                    String targetNo = detail.getCustomerNo();
+                    if (StringUtils.isBlank(targetNo)) {
+                        targetNo = detail.getObjectNo();
+                    }
+                    if (StringUtils.isNotBlank(targetNo)) {
+                        TeamMemberBo member = new TeamMemberBo();
+                        member.setObjectNo(targetNo);
+                        member.setUserNo(bo.getVisitorId());
+                        member.setRealName(bo.getVisitorName());
+                        member.setRoleCode("2"); // 业务员/普通成员
+                        member.setUpdateAccredit(0);
+                        
+                        // 映射关系: 0(客户)->12, 1(商机)->1, 2(年度入围)->2
+                        Integer relType = detail.getRelevanceType();
+                        int dataType = 12; // 默认客户
+                        if (relType != null) {
+                            if (relType == 1) dataType = 1;
+                            else if (relType == 2) dataType = 2;
+                        }
+                        member.setDataType(dataType);
+                        
+                        teamMemberService.insertOrUpdateMember(member);
+                    }
+                }
             }
         }
     }

+ 55 - 1
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerCareServiceImpl.java

@@ -9,15 +9,19 @@ import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.customer.controller.constant.CustomerConstants;
 import org.dromara.customer.domain.CustomerCare;
 import org.dromara.customer.domain.bo.CustomerCareBo;
 import org.dromara.customer.domain.vo.CustomerCareVo;
 import org.dromara.customer.mapper.CustomerCareMapper;
 import org.dromara.customer.service.ICustomerCareService;
+import org.dromara.customer.service.IOperationLogService;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
+import cn.hutool.core.date.DateUtil;
 
 /**
  * 客户关怀Service业务层处理
@@ -30,6 +34,7 @@ import java.util.List;
 public class CustomerCareServiceImpl implements ICustomerCareService {
 
     private final CustomerCareMapper baseMapper;
+    private final IOperationLogService operationLogService;
 
     /**
      * 查询客户关怀
@@ -63,6 +68,7 @@ public class CustomerCareServiceImpl implements ICustomerCareService {
         lqw.like(StringUtils.isNotBlank(bo.getReceiptId()), CustomerCare::getReceiptId, bo.getReceiptId());
         lqw.like(StringUtils.isNotBlank(bo.getCustomerName()), CustomerCare::getCustomerName, bo.getCustomerName());
         lqw.like(StringUtils.isNotBlank(bo.getContactPerson()), CustomerCare::getContactPerson, bo.getContactPerson());
+        lqw.eq(StringUtils.isNotBlank(bo.getPersonnelNumber()), CustomerCare::getPersonnelNumber, bo.getPersonnelNumber());
         lqw.eq(StringUtils.isNotBlank(bo.getConcernType()), CustomerCare::getConcernType, bo.getConcernType());
         lqw.eq(bo.getAuditStatus() != null, CustomerCare::getAuditStatus, bo.getAuditStatus());
         lqw.eq(StringUtils.isNotBlank(bo.getPlatformCode()), CustomerCare::getPlatformCode, bo.getPlatformCode());
@@ -79,10 +85,23 @@ public class CustomerCareServiceImpl implements ICustomerCareService {
     @Override
     public Boolean insertByBo(CustomerCareBo bo) {
         CustomerCare add = MapstructUtils.convert(bo, CustomerCare.class);
+        if (StringUtils.isBlank(add.getReceiptId())) {
+            add.setReceiptId(generateReceiptId());
+        }
+        add.setIsDelete(false); // 初始化逻辑删除状态
         validEntityBeforeSave(add);
         boolean flag = baseMapper.insert(add) > 0;
         if (flag) {
             bo.setId(add.getId());
+            // 记录日志
+            operationLogService.recordLog(
+                CustomerConstants.DATA_TYPE_CARE,
+                String.valueOf(add.getId()),
+                CustomerConstants.ACTION_TYPE_INSERT,
+                CustomerConstants.MODULE_CARE,
+                "新增了客户关怀单据:" + add.getReceiptId(),
+                add.getCustomerName()
+            );
         }
         return flag;
     }
@@ -94,7 +113,19 @@ public class CustomerCareServiceImpl implements ICustomerCareService {
     public Boolean updateByBo(CustomerCareBo bo) {
         CustomerCare update = MapstructUtils.convert(bo, CustomerCare.class);
         validEntityBeforeSave(update);
-        return baseMapper.updateById(update) > 0;
+        boolean flag = baseMapper.updateById(update) > 0;
+        if (flag) {
+            // 记录日志
+            operationLogService.recordLog(
+                CustomerConstants.DATA_TYPE_CARE,
+                String.valueOf(update.getId()),
+                CustomerConstants.ACTION_TYPE_UPDATE,
+                CustomerConstants.MODULE_CARE,
+                "修改了客户关怀信息",
+                update.getCustomerName()
+            );
+        }
+        return flag;
     }
 
     /**
@@ -114,4 +145,27 @@ public class CustomerCareServiceImpl implements ICustomerCareService {
         }
         return baseMapper.deleteByIds(ids) > 0;
     }
+
+    /**
+     * 生成单据编号:YYYYMMDD + 4位流水号
+     */
+    private String generateReceiptId() {
+        String dateStr = DateUtil.format(new Date(), "yyyyMMdd");
+        // 查询当天已有的最大编号
+        LambdaQueryWrapper<CustomerCare> lqw = Wrappers.lambdaQuery();
+        lqw.likeRight(CustomerCare::getReceiptId, dateStr);
+        lqw.orderByDesc(CustomerCare::getReceiptId);
+        lqw.last("limit 1");
+        CustomerCare lastOne = baseMapper.selectOne(lqw);
+        
+        int nextId = 1;
+        if (lastOne != null && StringUtils.isNotBlank(lastOne.getReceiptId())) {
+            String lastReceiptId = lastOne.getReceiptId();
+            if (lastReceiptId.length() >= 12) {
+                String suffix = lastReceiptId.substring(8);
+                nextId = Integer.parseInt(suffix) + 1;
+            }
+        }
+        return dateStr + String.format("%04d", nextId);
+    }
 }

+ 53 - 4
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerContactServiceImpl.java

@@ -10,7 +10,6 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboReference;
-import org.dromara.common.core.context.PlatformContext;
 import org.dromara.common.core.enums.SysPlatformYesNo;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
@@ -20,10 +19,12 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.redis.utils.SequenceUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.customer.domain.CustomerContact;
+import org.dromara.customer.domain.CustomerContactInfo;
 import org.dromara.customer.domain.CustomerInfo;
 import org.dromara.customer.domain.bo.CustomerContactBo;
 import org.dromara.customer.domain.vo.CustomerContactVo;
 import org.dromara.customer.domain.vo.CustomerInfoVo;
+import org.dromara.customer.mapper.CustomerContactInfoMapper;
 import org.dromara.customer.mapper.CustomerContactMapper;
 import org.dromara.customer.mapper.CustomerInfoMapper;
 import org.dromara.customer.service.ICustomerContactService;
@@ -59,6 +60,8 @@ public class CustomerContactServiceImpl extends ServiceImpl<CustomerContactMappe
 
     private final CustomerInfoMapper customerInfoMapper;
 
+    private final CustomerContactInfoMapper contactInfoMapper;
+
     /**
      * 查询客户联系人信息
      *
@@ -67,7 +70,17 @@ public class CustomerContactServiceImpl extends ServiceImpl<CustomerContactMappe
      */
     @Override
     public CustomerContactVo queryById(Long id) {
-        return baseMapper.selectVoById(id);
+        CustomerContactVo vo = baseMapper.selectVoById(id);
+        if (vo != null) {
+            // 查询扩展信息
+            CustomerContactInfo info = contactInfoMapper.selectOne(new LambdaQueryWrapper<CustomerContactInfo>()
+                .eq(CustomerContactInfo::getContactId, id));
+            if (info != null) {
+                // 将扩展信息字段合并到 VO 中
+                MapstructUtils.convert(info, vo);
+            }
+        }
+        return vo;
     }
 
     @Override
@@ -254,6 +267,14 @@ public class CustomerContactServiceImpl extends ServiceImpl<CustomerContactMappe
         boolean flag = baseMapper.insert(add) > 0;
         if (flag) {
             bo.setId(add.getId());
+            try {
+                // 保存扩展信息
+                CustomerContactInfo info = MapstructUtils.convert(bo, CustomerContactInfo.class);
+                info.setContactId(add.getId());
+                contactInfoMapper.insert(info);
+            } catch (Exception e) {
+                log.warn("保存联系人扩展信息失败,可能表 customer_contact_info 尚未创建: {}", e.getMessage());
+            }
         }
         return flag;
     }
@@ -277,7 +298,25 @@ public class CustomerContactServiceImpl extends ServiceImpl<CustomerContactMappe
             remoteUserBo.setRoleId(update.getRoleId());
             remoteUserService.addUserRole(remoteUserBo);
         }
-        return baseMapper.updateById(update) > 0;
+        boolean flag = baseMapper.updateById(update) > 0;
+        if (flag) {
+            try {
+                // 更新或插入扩展信息
+                CustomerContactInfo info = MapstructUtils.convert(bo, CustomerContactInfo.class);
+                info.setContactId(bo.getId());
+                CustomerContactInfo exist = contactInfoMapper.selectOne(new LambdaQueryWrapper<CustomerContactInfo>()
+                    .eq(CustomerContactInfo::getContactId, bo.getId()));
+                if (exist != null) {
+                    info.setId(exist.getId());
+                    contactInfoMapper.updateById(info);
+                } else {
+                    contactInfoMapper.insert(info);
+                }
+            } catch (Exception e) {
+                log.warn("更新联系人扩展信息失败,可能表 customer_contact_info 尚未创建: {}", e.getMessage());
+            }
+        }
+        return flag;
     }
 
     @Override
@@ -368,7 +407,17 @@ public class CustomerContactServiceImpl extends ServiceImpl<CustomerContactMappe
             }
         }*/  //联系人删除时不删除用户  后续可能会复用之前的 用户
         // 7. 执行本地数据库删除
-        int rows = baseMapper.deleteByIds(ids); // 确保 MP 版本支持或自定义 XML
+        int rows = baseMapper.deleteByIds(ids);
+        if (rows > 0) {
+            try {
+                // 同步删除扩展信息
+                contactInfoMapper.delete(new LambdaQueryWrapper<CustomerContactInfo>()
+                    .in(CustomerContactInfo::getContactId, ids));
+            } catch (Exception e) {
+                // 如果表不存在或删除失败,记录日志但不抛出异常
+                log.warn("同步删除联系人扩展信息失败,可能表 customer_contact_info 尚未创建: {}", e.getMessage());
+            }
+        }
 
         return rows > 0;
     }

+ 174 - 22
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerInfoServiceImpl.java

@@ -34,7 +34,9 @@ import org.dromara.customer.utils.qcc.domain.CompanyInfoResponse;
 import org.dromara.system.api.*;
 import org.dromara.system.api.domain.bo.RemoteUserBo;
 import org.dromara.system.api.domain.vo.RemoteDeptVo;
+import org.dromara.system.api.domain.vo.RemoteComStaffVo;
 import org.dromara.system.api.domain.vo.RemoteDictDataVo;
+import org.dromara.system.api.domain.dto.AddressAreaDTO;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -82,6 +84,9 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
     @DubboReference
     private RemoteDictService remoteDictService;
 
+    @DubboReference
+    private RemoteAddressAreaService remoteAddressAreaService;
+
     private static final String CUSTOMER_NO_KEY = "customer_info:customer_no";
     private static final String CONTACT_NO_KEY = "customer_contact:contact_no";
 
@@ -96,6 +101,9 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
     private final CustomerContractMapper customerContractMapper;
     private final CustomerDeptMapper customerDeptMapper;
     private final CustomerDictMapper customerDictMapper;
+    private final SupplierBusinessInfoMapper supplierBusinessInfoMapper;
+    private final TeamMemberMapper teamMemberMapper;
+    private final CrmStaffMapper crmStaffMapper;
 
 
     /**
@@ -115,14 +123,21 @@ 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());
+
+        // 补充基础名称字段
+        if (vo.getEnterpriseScaleId() != 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.getIndustryCategoryId() != null) {
+            IndustryCategoryVo industryCategoryVo = industryCategoryMapper.selectVoById(vo.getIndustryCategoryId());
+            if (industryCategoryVo != null) {
+                vo.setIndustryCategory(industryCategoryVo.getIndustryCategoryName());
+                vo.setIndustryName(industryCategoryVo.getIndustryCategoryName());
+            }
         }
         // 2. 查询关联信息
         vo.setCustomerBusinessInfoVo(
@@ -131,6 +146,80 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
                 .orElse(null)
         );
 
+        // 企业类型名称 (Q0001)
+        if (vo.getCustomerTypeId() != null) {
+            CustomerDict dict = customerDictMapper.selectById(vo.getCustomerTypeId());
+            if (dict != null) {
+                vo.setEnterpriseTypeName(dict.getName());
+            }
+        }
+
+        // 客户等级名称 (远程调用)
+        if (vo.getCustomerLevelId() != null) {
+            Map<Long, String> levelMap = remoteComCustomerLevelService.selectCustomerLevelNameByIds(Collections.singleton(vo.getCustomerLevelId()));
+            vo.setCustomerLevelName(levelMap.get(vo.getCustomerLevelId()));
+        }
+
+        // 合作状态名称 (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()));
+        }
+
+        // 补充省份和城市名称
+        Set<Long> areaIds = new HashSet<>();
+        if (StringUtils.isNotBlank(vo.getRegProvincialNo()) && StringUtils.isNumeric(vo.getRegProvincialNo())) {
+            areaIds.add(Long.parseLong(vo.getRegProvincialNo()));
+        }
+        if (StringUtils.isNotBlank(vo.getRegCityNo()) && StringUtils.isNumeric(vo.getRegCityNo())) {
+            areaIds.add(Long.parseLong(vo.getRegCityNo()));
+        }
+        if (!areaIds.isEmpty()) {
+            List<AddressAreaDTO> areaList = remoteAddressAreaService.listByIds(areaIds);
+            Map<String, String> areaMap = areaList.stream().collect(Collectors.toMap(d -> String.valueOf(d.getId()), AddressAreaDTO::getAreaName));
+            vo.setRegProvincialName(areaMap.get(vo.getRegProvincialNo()));
+            vo.setRegCityName(areaMap.get(vo.getRegCityNo()));
+        }
+
+        // 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());
+                businessInfo.setSocialCreditCode(supplierBiz.getSocialCreditCode());
+                businessInfo.setLegalPersonName(supplierBiz.getLegalPersonName());
+                businessInfo.setRegisteredCapital(supplierBiz.getRegisteredCapital());
+                businessInfo.setEstablishmentDate(supplierBiz.getEstablishmentDate());
+                businessInfo.setRevocationDate(supplierBiz.getRevocationDate());
+                businessInfo.setRegistrationStatus(supplierBiz.getRegistrationStatus());
+                businessInfo.setBusinessAddress(supplierBiz.getBusinessAddress());
+                businessInfo.setBusinessLicense(supplierBiz.getBusinessLicense());
+            }
+        }
+
+        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());
+        }
+
+        // 销售信息
         CustomerSalesInfo customerSalesInfo = customerSalesInfoMapper.selectByCustomerId(id);
         if (customerSalesInfo == null) {
             vo.setCustomerSalesInfoVo(null);
@@ -199,6 +288,11 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
         vo.setBelongingDepartmentName(comDeptMap.get(vo.getBelongingDepartmentId()));
 
 
+        // 补充开票类型 (来自主表 sell_invoice_type)
+        vo.setInvoiceType(vo.getSellInvoiceType());
+        vo.setInvoiceTypeName(vo.getSellInvoiceType());
+
+        // 联系人列表
         List<CustomerContact> contactEntities = customerContactMapper.selectListByCustomerId(id);
         vo.setCustomerContactVoList(
             contactEntities != null
@@ -208,6 +302,7 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
                 : Collections.emptyList()
         );
 
+        // 发票列表
         List<CustomerInvoiceInfo> invoiceEntities = customerInvoiceInfoMapper.selectListByCustomerId(id);
         vo.setCustomerInvoiceInfoVoList(
             invoiceEntities != null
@@ -232,6 +327,28 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
                 }
             });
         }
+        // 4. 补全管理信息人名
+        Set<Long> userIds = new HashSet<>();
+        if (StringUtils.isNotBlank(vo.getCreateBy()) && StringUtils.isNumeric(vo.getCreateBy())) {
+            userIds.add(Long.parseLong(vo.getCreateBy()));
+        }
+        if (StringUtils.isNotBlank(vo.getUpdateBy()) && StringUtils.isNumeric(vo.getUpdateBy())) {
+            userIds.add(Long.parseLong(vo.getUpdateBy()));
+        }
+        if (!userIds.isEmpty()) {
+            Map<Long, String> userMap = remoteUserService.selectUserNamesByIds(new ArrayList<>(userIds));
+            if (vo.getCreateBy() != null) {
+                String name = userMap.get(Long.parseLong(vo.getCreateBy()));
+                if (name == null && "-1".equals(vo.getCreateBy())) name = "系统";
+                vo.setCreateByName(name);
+            }
+            if (vo.getUpdateBy() != null) {
+                String name = userMap.get(Long.parseLong(vo.getUpdateBy()));
+                if (name == null && "-1".equals(vo.getUpdateBy())) name = "系统";
+                vo.setUpdateByName(name);
+            }
+        }
+
         return vo;
     }
 
@@ -365,6 +482,9 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
             customerVo.setCompanyName(companyMap.get(customerVo.getBelongCompanyId()));
             CustomerSalesInfoVo salesVo = salesInfoMap.get(customerVo.getId());
             customerVo.setCustomerSalesInfoVo(salesVo);
+            if (salesVo != null) {
+                salesVo.setBelongingDepartment(customerVo.getDeptName());
+            }
         }
 
         return TableDataInfo.build(result);
@@ -525,10 +645,6 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
      */
     @Override
     public TableDataInfo<CustomerListVo> queryCustomerListPage(CustomerListBo bo, PageQuery pageQuery) {
-        // 设置平台标识(暂时注释掉)
-        // if (StringUtils.isBlank(bo.getPlatformCode())) {
-        //     bo.setPlatformCode(PlatformContext.getPlatform());
-        // }
         // 强制处理 isHighSeas 参数逻辑
         Object isHighSeas = StringUtils.isNotBlank(bo.getIsHighSeas()) ? bo.getIsHighSeas() : bo.getParams().get("isHighSeas");
         if (isHighSeas == null) {
@@ -556,16 +672,20 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
                 if (!"true".equals(bo.getIsHighSeas())) {
                     if (vo.getSalesPersonId() != null) staffIds.add(vo.getSalesPersonId());
                     if (vo.getServiceStaffId() != null) staffIds.add(vo.getServiceStaffId());
-                    if (vo.getBelongingDepartmentId() != null) deptIds.add(vo.getBelongingDepartmentId());
                     if (vo.getCreditLevelId() != null) creditLevelIds.add(vo.getCreditLevelId());
                 }
                 if (vo.getBelongCompanyId() != null) companyIds.add(vo.getBelongCompanyId());
                 if (vo.getEnterpriseTypeId() != null) enterpriseTypeIds.add(vo.getEnterpriseTypeId());
                 if (vo.getCustomerLevelId() != null) customerLevelIds.add(vo.getCustomerLevelId());
+
+                // 处理部门ID收集
+                if (vo.getBelongingDepartmentId() != null) {
+                    deptIds.add(vo.getBelongingDepartmentId());
+                }
             });
 
             // 1. 批量查询业务员和客服名称 (远程调用)
-            Map<Long, String> staffMap = staffIds.isEmpty() ? Collections.emptyMap() : remoteErpStaffService.selectStaffNameByIds(staffIds);
+            Map<Long, String> staffMap = staffIds.isEmpty() ? Collections.emptyMap() : remoteComStaffService.selectStaffNameByIds(staffIds);
 
             // 2. 批量查询信用等级名称 (远程调用)
             Map<Long, String> creditLevelMap = creditLevelIds.isEmpty() ? Collections.emptyMap() : remoteCreditLevelService.selectCreditLevelNameByIds(creditLevelIds);
@@ -581,8 +701,8 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
             // 5. 批量查询客户/合作等级名称 (远程调用)
             Map<Long, String> customerLevelMap = customerLevelIds.isEmpty() ? Collections.emptyMap() : remoteComCustomerLevelService.selectCustomerLevelNameByIds(customerLevelIds);
 
-            // 6. 批量查询部门名称 (远程调用)
-            Map<Long, String> deptMap = deptIds.isEmpty() ? Collections.emptyMap() : remoteErpDeptService.selectDeptNameByIds(deptIds);
+            // 6. 批量查询部门名称 (系统部门)
+            Map<Long, String> deptMap = deptIds.isEmpty() ? Collections.emptyMap() : remoteDeptService.selectDeptNameByIds(deptIds);
 
             // 7. 批量查询合作状态名称 (从客户字典表查询 code='COOPERATION_STATUS')
             List<CustomerDict> cooperationDicts = customerDictMapper.selectList(new LambdaQueryWrapper<CustomerDict>()
@@ -609,10 +729,9 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
                 vo.setCompanyName(companyMap.get(vo.getBelongCompanyId()));
                 vo.setEnterpriseTypeName(enterpriseTypeMap.get(vo.getEnterpriseTypeId()));
                 vo.setCustomerLevelName(customerLevelMap.get(vo.getCustomerLevelId()));
-                if (StringUtils.isBlank(vo.getDeptName())) {
-                    vo.setDeptName(deptMap.get(vo.getBelongingDepartmentId()));
-                }
                 vo.setCooperationName(cooperationMap.get(vo.getStatus()));
+
+                // 部门名称已在 Mapper 中通过业务负责人关联查询,此处无需再回填覆盖
             });
         }
 
@@ -670,7 +789,7 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
             // 公海客户:负责人和客服均为空(或为0)
             lqw.and(w -> w.and(a -> a.isNull(CustomerInfo::getSalesPersonId).or().eq(CustomerInfo::getSalesPersonId, 0))
                          .and(b -> b.isNull(CustomerInfo::getServiceStaffId).or().eq(CustomerInfo::getServiceStaffId, 0)));
-        } else {
+        } else if ("false".equals(isHighSeasStr)) {
             // 有效客户:负责人不为空(且不为0) OR 客服不为空(且不为0)
             lqw.and(w -> w.and(a -> a.isNotNull(CustomerInfo::getSalesPersonId).ne(CustomerInfo::getSalesPersonId, 0))
                          .or(b -> b.isNotNull(CustomerInfo::getServiceStaffId).ne(CustomerInfo::getServiceStaffId, 0)));
@@ -1519,14 +1638,47 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
     @Transactional(rollbackFor = Exception.class)
     public Boolean claimPool(CustomerClaimBo claimBo) {
         Long customerId = claimBo.getId();
-        // 认领操作仅更新客户主表,不再涉及到销售信息表
-        CustomerInfo customer = new CustomerInfo();
-        customer.setId(customerId);
+        CustomerInfo customer = baseMapper.selectById(customerId);
+        if (customer == null) {
+            return false;
+        }
+
+        // 1. 认领操作更新客户主表
         customer.setSalesPersonId(claimBo.getSalesPersonId());
         customer.setServiceStaffId(claimBo.getServiceStaffId());
         customer.setBelongingDepartmentId(claimBo.getDeptId());
         customer.setStatus("0"); // 认领成功后,将客户主表状态改为有效
-        return baseMapper.updateById(customer) > 0;
+        boolean updated = baseMapper.updateById(customer) > 0;
+
+        if (updated && claimBo.getSalesPersonId() != null) {
+            // 2. 将业务员加入团队成员
+            // 检查是否已在团队中
+            Long count = teamMemberMapper.selectCount(new LambdaQueryWrapper<TeamMember>()
+                .eq(TeamMember::getDataType, 12)
+                .eq(TeamMember::getObjectNo, customer.getCustomerNo())
+                .eq(TeamMember::getUserNo, claimBo.getSalesPersonId()));
+
+            if (count == 0) {
+                TeamMember member = new TeamMember();
+                member.setDataType(12); // 客户类型
+                member.setObjectNo(customer.getCustomerNo());
+                member.setUserNo(claimBo.getSalesPersonId());
+                member.setRoleCode("B0001"); // 业务负责人
+                member.setIzManager(1);
+                member.setUpdateAccredit(1);
+                member.setPlatformCode(PlatformContext.getPlatform());
+
+                // 获取真实姓名:直接从业务人员表获取
+                CrmStaff staff = crmStaffMapper.selectById(claimBo.getSalesPersonId());
+                if (staff != null) {
+                    member.setRealName(staff.getStaffName());
+                }
+
+                teamMemberMapper.insert(member);
+            }
+        }
+
+        return updated;
     }
 
     @Override

+ 16 - 1
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/OperationLogServiceImpl.java

@@ -2,6 +2,7 @@ package org.dromara.customer.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.ServletUtils;
@@ -12,6 +13,8 @@ import org.dromara.customer.domain.bo.OperationLogBo;
 import org.dromara.customer.domain.vo.OperationLogVo;
 import org.dromara.customer.mapper.OperationLogMapper;
 import org.dromara.customer.service.IOperationLogService;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
@@ -27,9 +30,21 @@ import java.util.List;
 public class OperationLogServiceImpl implements IOperationLogService {
 
     private final OperationLogMapper baseMapper;
+    
+    @Override
+    public TableDataInfo<OperationLogVo> queryPageList(OperationLogBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<OperationLog> lqw = buildQueryWrapper(bo);
+        Page<OperationLogVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
 
     @Override
     public List<OperationLogVo> queryList(OperationLogBo bo) {
+        LambdaQueryWrapper<OperationLog> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<OperationLog> buildQueryWrapper(OperationLogBo bo) {
         LambdaQueryWrapper<OperationLog> lqw = Wrappers.lambdaQuery();
         // 如果传入了 dataType 则匹配,否则默认查销售线索相关的 (1)
         lqw.eq(OperationLog::getDataType, bo.getDataType() != null ? bo.getDataType() : 1);
@@ -38,7 +53,7 @@ public class OperationLogServiceImpl implements IOperationLogService {
         lqw.eq(bo.getActionModule() != null, OperationLog::getActionModule, bo.getActionModule());
         lqw.eq(StringUtils.isNotBlank(bo.getPlatformCode()), OperationLog::getPlatformCode, bo.getPlatformCode());
         lqw.orderByDesc(OperationLog::getCreateTime);
-        return baseMapper.selectVoList(lqw);
+        return lqw;
     }
 
     @Override

+ 30 - 8
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SalesAnnualFinalizationServiceImpl.java

@@ -96,6 +96,9 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
         return baseMapper.selectVoById(id);
     }
 
+    /**
+     * 新增年度入围项目
+     */
     /**
      * 新增年度入围项目
      */
@@ -109,6 +112,7 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
             add.setProjectNo(SequenceUtils.nextPaddedIdStr(PROJECT_NO_KEY, Duration.ofDays(3650), 6));
         }
 
+        // 必须保留的逻辑:根据客户名称补全客户编号,否则数据库会报非空约束错误
         if (StrUtil.isBlank(add.getCustomNo()) && StrUtil.isNotBlank(add.getCustomName())) {
             CustomerInfoVo customerInfo = customerInfoService.selectCustomerByName(add.getCustomName());
             if (customerInfo != null && StrUtil.isNotBlank(customerInfo.getCustomerNo())) {
@@ -119,12 +123,17 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
         boolean flag = baseMapper.insert(add) > 0;
         if (flag) {
             bo.setId(add.getId());
-            operationLogService.recordLog(2, String.valueOf(add.getId()), 1, null, "年度入围", add.getProjectName());
+            Integer logDataType = Integer.valueOf(2).equals(add.getFinalizationType()) ? 
+                CustomerConstants.DATA_TYPE_PROJECT_SELECTION : CustomerConstants.DATA_TYPE_PLATFORM_SELECTION;
+            operationLogService.recordLog(logDataType, String.valueOf(add.getId()), CustomerConstants.ACTION_TYPE_INSERT, null, "年度入围", add.getProjectName());
             
-            // 自动将负责人加入团队成员
+            // 简单将负责人加入团队成员
             if (add.getLeader() != null) {
                 TeamMemberBo memberBo = new TeamMemberBo();
-                memberBo.setDataType(2); // 年度入围
+                Integer dataType = Integer.valueOf(2).equals(add.getFinalizationType()) ? 
+                    CustomerConstants.DATA_TYPE_PROJECT_SELECTION : CustomerConstants.DATA_TYPE_PLATFORM_SELECTION;
+                
+                memberBo.setDataType(dataType);
                 memberBo.setObjectNo(String.valueOf(add.getId()));
                 memberBo.setUserNo(add.getLeader());
                 memberBo.setRealName(add.getLeaderName());
@@ -146,15 +155,22 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
         SalesAnnualFinalization oldData = baseMapper.selectById(bo.getId());
         SalesAnnualFinalization update = BeanUtil.toBean(bo, SalesAnnualFinalization.class);
         validEntityBeforeSave(update);
+
         boolean flag = baseMapper.updateById(update) > 0;
         if (flag && oldData != null) {
+            Integer logDataType = Integer.valueOf(2).equals(update.getFinalizationType()) ? 
+                CustomerConstants.DATA_TYPE_PROJECT_SELECTION : CustomerConstants.DATA_TYPE_PLATFORM_SELECTION;
+            
             if (!ObjectUtil.equals(oldData.getProjectStatus(), bo.getProjectStatus())) {
-                operationLogService.recordLog(2, String.valueOf(bo.getId()), 2, null, "项目状态", String.valueOf(bo.getProjectStatus()));
+                operationLogService.recordLog(logDataType, String.valueOf(bo.getId()), CustomerConstants.ACTION_TYPE_UPDATE, null, "项目状态", String.valueOf(bo.getProjectStatus()));
             }
+
             // 同步负责人到团队成员
             if (update.getLeader() != null) {
                 TeamMemberBo memberBo = new TeamMemberBo();
-                memberBo.setDataType(2);
+                Integer dataType = Integer.valueOf(2).equals(update.getFinalizationType()) ? 
+                    CustomerConstants.DATA_TYPE_PROJECT_SELECTION : CustomerConstants.DATA_TYPE_PLATFORM_SELECTION;
+                memberBo.setDataType(dataType);
                 memberBo.setObjectNo(String.valueOf(update.getId()));
                 memberBo.setUserNo(update.getLeader());
                 memberBo.setRealName(update.getLeaderName());
@@ -224,7 +240,9 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
             
             // 如果不保留原负责人,且原负责人与新负责人不同,则从团队中移除原负责人
             if (Boolean.FALSE.equals(bo.getKeepAsMember()) && oldLeaderId != null && !oldLeaderId.equals(newLeaderId)) {
-                teamMemberService.removeMember(2, String.valueOf(id), oldLeaderId);
+                Integer dataType = Integer.valueOf(2).equals(oldData.getFinalizationType()) ? 
+                    CustomerConstants.DATA_TYPE_PROJECT_SELECTION : CustomerConstants.DATA_TYPE_PLATFORM_SELECTION;
+                teamMemberService.removeMember(dataType, String.valueOf(id), oldLeaderId);
             }
 
             SalesAnnualFinalization update = new SalesAnnualFinalization();
@@ -236,7 +254,9 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
             if (success) {
                 // 同步新负责人到团队成员
                 TeamMemberBo memberBo = new TeamMemberBo();
-                memberBo.setDataType(2); // 年度入围
+                Integer dataType = Integer.valueOf(2).equals(oldData.getFinalizationType()) ? 
+                    CustomerConstants.DATA_TYPE_PROJECT_SELECTION : CustomerConstants.DATA_TYPE_PLATFORM_SELECTION;
+                memberBo.setDataType(dataType);
                 memberBo.setObjectNo(String.valueOf(id));
                 memberBo.setUserNo(newLeaderId);
                 memberBo.setRealName(bo.getLeaderName());
@@ -245,8 +265,10 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
                 memberBo.setIzManager(1);
                 teamMemberService.insertOrUpdateMember(memberBo);
 
+                Integer logDataType = Integer.valueOf(2).equals(oldData.getFinalizationType()) ? 
+                    CustomerConstants.DATA_TYPE_PROJECT_SELECTION : CustomerConstants.DATA_TYPE_PLATFORM_SELECTION;
                 // 记录日志
-                operationLogService.recordLog(2, String.valueOf(id), CustomerConstants.ACTION_TYPE_TRANSFER, null, "转移了年度入围项目", bo.getLeaderName());
+                operationLogService.recordLog(logDataType, String.valueOf(id), CustomerConstants.ACTION_TYPE_TRANSFER, null, "转移了年度入围项目", bo.getLeaderName());
             }
         }
         return result;

+ 26 - 1
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SalesleadsServiceImpl.java

@@ -110,7 +110,18 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
             operationLogService.recordLog(CustomerConstants.DATA_TYPE_LEADS, String.valueOf(bo.getId()), CustomerConstants.ACTION_TYPE_UPDATE, null, "修改了销售线索", bo.getProjectName());
             
             if (!ObjectUtils.equals(oldData.getProjectSchedule(), bo.getProjectSchedule())) {
-                operationLogService.recordLog(CustomerConstants.DATA_TYPE_LEADS, String.valueOf(bo.getId()), CustomerConstants.ACTION_TYPE_UPDATE, null, CustomerConstants.MODULE_PROJECT_PROGRESS, bo.getProjectSchedule());
+                String oldS = oldData.getProjectSchedule();
+                String newS = bo.getProjectSchedule();
+                String action = "修改了";
+                if (oldS != null && newS != null) {
+                    try {
+                        int oldV = Integer.parseInt(oldS);
+                        int newV = Integer.parseInt(newS);
+                        action = newV > oldV ? "同意" : "退回";
+                    } catch (Exception e) {}
+                }
+                String content = action + ":" + getScheduleName(newS);
+                operationLogService.recordLog(CustomerConstants.DATA_TYPE_LEADS, String.valueOf(bo.getId()), CustomerConstants.ACTION_TYPE_UPDATE, null, content, "");
             }
         }
         if (success && bo.getLeader() != null) {
@@ -246,4 +257,18 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
         teamMemberService.insertOrUpdateMember(memberBo);
     }
 
+    /**
+     * 获取项目进度名称
+     */
+    private String getScheduleName(String schedule) {
+        if (schedule == null) return "";
+        return switch (schedule) {
+            case "1" -> "标前磋商";
+            case "2" -> "立项公示";
+            case "3" -> "应标投标";
+            case "4" -> "结案";
+            default -> schedule;
+        };
+    }
+
 }

+ 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;
     }
 }

+ 3 - 1
ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/SalesleadsMapper.xml

@@ -5,12 +5,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 <mapper namespace="org.dromara.customer.mapper.SalesleadsMapper">
 
     <select id="selectSalesleadsList" resultType="org.dromara.customer.domain.vo.SalesleadsVo">
-        SELECT s.*, cc.company_name AS companyName, ic.industry_category_name AS industry, d.dept_name AS deptName
+        SELECT s.*, cc.company_name AS companyName, ic.industry_category_name AS industry, d.dept_name AS deptName,
+               CASE sra.dealResult WHEN 1 THEN '赢单' WHEN 2 THEN '丢单' ELSE sra.dealResult END AS dealResult
         FROM salesleads s
         LEFT JOIN sys_company cc ON s.company_no COLLATE utf8mb4_unicode_ci = cc.company_code COLLATE utf8mb4_unicode_ci AND cc.del_flag = '0'
         LEFT JOIN industry_category ic ON s.profession = ic.id AND ic.del_flag = '0'
         LEFT JOIN com_staff cs ON s.leader = cs.staff_id
         LEFT JOIN sys_dept d ON cs.dept_id = d.dept_id
+        LEFT JOIN salesresultanalyze sra ON s.project_no COLLATE utf8mb4_unicode_ci = sra.objectNo COLLATE utf8mb4_unicode_ci AND (sra.IsDelete = 0 OR sra.IsDelete IS NULL)
         <where>
             s.del_flag = '0'
             <if test="bo.id != null">

+ 6 - 3
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/ComCustomerLevelVo.java

@@ -5,15 +5,15 @@ import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
 import cn.idev.excel.annotation.ExcelProperty;
 import org.dromara.common.excel.annotation.ExcelDictFormat;
 import org.dromara.common.excel.convert.ExcelDictConvert;
+import org.dromara.system.api.domain.vo.RemoteComCustomerLevelVo;
 import io.github.linpeilie.annotations.AutoMapper;
+import io.github.linpeilie.annotations.AutoMappers;
 import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
 import java.util.Date;
 
-
-
 /**
  * 客户等级配置视图对象 com_customer_level
  *
@@ -22,7 +22,10 @@ import java.util.Date;
  */
 @Data
 @ExcelIgnoreUnannotated
-@AutoMapper(target = ComCustomerLevel.class)
+@AutoMappers({
+    @AutoMapper(target = ComCustomerLevel.class),
+    @AutoMapper(target = RemoteComCustomerLevelVo.class)
+})
 public class ComCustomerLevelVo implements Serializable {
 
     @Serial

+ 6 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/ComCustomerTypeVo.java

@@ -5,7 +5,9 @@ import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
 import cn.idev.excel.annotation.ExcelProperty;
 import org.dromara.common.excel.annotation.ExcelDictFormat;
 import org.dromara.common.excel.convert.ExcelDictConvert;
+import org.dromara.system.api.domain.vo.RemoteComCustomerTypeVo;
 import io.github.linpeilie.annotations.AutoMapper;
+import io.github.linpeilie.annotations.AutoMappers;
 import lombok.Data;
 
 import java.io.Serial;
@@ -22,7 +24,10 @@ import java.util.Date;
  */
 @Data
 @ExcelIgnoreUnannotated
-@AutoMapper(target = ComCustomerType.class)
+@AutoMappers({
+    @AutoMapper(target = ComCustomerType.class),
+    @AutoMapper(target = RemoteComCustomerTypeVo.class)
+})
 public class ComCustomerTypeVo implements Serializable {
 
     @Serial

+ 2 - 3
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteComCustomerLevelServiceImpl.java

@@ -3,13 +3,11 @@ package org.dromara.system.dubbo;
 import cn.hutool.core.bean.BeanUtil;
 import lombok.RequiredArgsConstructor;
 import org.apache.dubbo.config.annotation.DubboService;
-import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.system.api.RemoteComCustomerLevelService;
 import org.dromara.system.api.domain.vo.RemoteComCustomerLevelVo;
 import org.dromara.system.domain.bo.ComCustomerLevelBo;
 import org.dromara.system.domain.vo.ComCustomerLevelVo;
 import org.dromara.system.service.IComCustomerLevelService;
-import org.dromara.common.core.utils.MapstructUtils;
 import org.springframework.stereotype.Service;
 
 import cn.hutool.core.collection.CollUtil;
@@ -45,6 +43,7 @@ public class RemoteComCustomerLevelServiceImpl implements RemoteComCustomerLevel
 
     @Override
     public List<RemoteComCustomerLevelVo> selectCustomerLevelList() {
-        return MapstructUtils.convert(comCustomerLevelService.queryList(new ComCustomerLevelBo()), RemoteComCustomerLevelVo.class);
+        List<ComCustomerLevelVo> list = comCustomerLevelService.queryList(new ComCustomerLevelBo());
+        return BeanUtil.copyToList(list, RemoteComCustomerLevelVo.class);
     }
 }