瀏覽代碼

Merge remote-tracking branch 'origin/master' into master

# Conflicts:
#	ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlatformDataScopeInterceptor.java
肖路 3 天之前
父節點
當前提交
e0a8fa1211
共有 19 個文件被更改,包括 1184 次插入92 次删除
  1. 1 0
      ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlatformDataScopeInterceptor.java
  2. 181 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/CustomerPoolController.java
  3. 12 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/CustomerListBo.java
  4. 6 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/SalesAnnualFinalizationBo.java
  5. 6 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/SalesleadsBo.java
  6. 23 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/mapper/CustomerPoolMapper.java
  7. 9 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/mapper/TeamMemberMapper.java
  8. 59 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/ICustomerPoolService.java
  9. 169 79
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerInfoServiceImpl.java
  10. 247 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerPoolServiceImpl.java
  11. 114 0
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SalesAnnualFinalizationServiceImpl.java
  12. 158 1
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SalesleadsServiceImpl.java
  13. 31 1
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/TeamMemberServiceImpl.java
  14. 26 11
      ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/WorkbenchServiceImpl.java
  15. 9 0
      ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/CustomerInfoMapper.xml
  16. 109 0
      ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/CustomerPoolMapper.xml
  17. 11 0
      ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/SalesAnnualFinalizationMapper.xml
  18. 6 0
      ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/SalesleadsMapper.xml
  19. 7 0
      ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/TeamMemberMapper.xml

+ 1 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/PlatformDataScopeInterceptor.java

@@ -127,6 +127,7 @@ public class PlatformDataScopeInterceptor implements Interceptor {
         "maintain_info",
         "maintenance_server_item",
         "contract_supply",
+        "team_member",
         "ep_"
         // 注意:前缀匹配需特殊处理(如 qrtz_),见 isIgnoreTable 方法
     ));

+ 181 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/controller/CustomerPoolController.java

@@ -0,0 +1,181 @@
+package org.dromara.customer.controller;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+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.common.web.core.BaseController;
+import org.dromara.customer.domain.bo.CustomerClaimBo;
+import org.dromara.customer.domain.bo.CustomerInfoBo;
+import org.dromara.customer.domain.bo.CustomerListBo;
+import org.dromara.customer.domain.dto.SetCustomerInfoTagDto;
+import org.dromara.customer.domain.dto.ReleaseToPoolDto;
+import org.dromara.customer.domain.vo.CustomerInfoVo;
+import org.dromara.customer.domain.vo.CustomerListVo;
+import org.dromara.customer.service.ICustomerPoolService;
+import org.dromara.system.api.RemoteComCompanyService;
+import org.dromara.system.api.RemoteComCustomerLevelService;
+import org.dromara.system.api.RemoteComStaffService;
+import org.dromara.system.api.domain.vo.RemoteComCompanyVo;
+import org.dromara.system.api.domain.vo.RemoteComCustomerLevelVo;
+import org.dromara.system.api.domain.vo.RemoteComStaffVo;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 客户公海与有效客户统一控制器
+ *
+ * @author Antigravity
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/customerPool")
+public class CustomerPoolController extends BaseController {
+
+    @DubboReference
+    private RemoteComCompanyService remoteComCompanyService;
+    @DubboReference
+    private RemoteComCustomerLevelService remoteComCustomerLevelService;
+    @DubboReference
+    private RemoteComStaffService remoteComStaffService;
+
+    private final ICustomerPoolService customerPoolService;
+
+    /**
+     * 查询客户公海/有效客户列表 (支持通用 isHighSeas 参数)
+     */
+    @GetMapping("/customerList")
+    public TableDataInfo<CustomerListVo> customerList(CustomerListBo bo, PageQuery pageQuery) {
+        return customerPoolService.queryCustomerListPage(bo, pageQuery);
+    }
+
+    /**
+     * 查询有效客户列表 (兼容原 /list,默认过滤有效客户,带数据权限)
+     */
+    @GetMapping("/list")
+    public TableDataInfo<CustomerListVo> list(CustomerListBo bo, PageQuery pageQuery) {
+        Long userId = LoginHelper.getLoginUser().getUserId();
+        RemoteComStaffVo remoteComStaffVo = remoteComStaffService.selectStaffByUserId(userId);
+        Long staffId = (remoteComStaffVo != null) ? remoteComStaffVo.getStaffId() : null;
+
+        boolean isAdmin = LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin();
+        bo.getParams().put("isAdmin", isAdmin ? "true" : "false");
+        bo.getParams().put("createBy", userId);
+
+        if ("mine".equals(bo.getActiveTab())) {
+            bo.setSalesPersonId(staffId != null ? staffId : -1L);
+            bo.setServiceStaffId(null);
+            bo.setCreateBy(null);
+        } else if ("involved".equals(bo.getActiveTab())) {
+            bo.setServiceStaffId(staffId != null ? staffId : -1L);
+            bo.setSalesPersonId(null);
+            bo.setCreateBy(null);
+        } else {
+            // "all" tab
+            if (isAdmin) {
+                // 管理员在"全部客户"时查看所有数据,不加自己ID的限制
+                bo.setSalesPersonId(null);
+                bo.setServiceStaffId(null);
+                bo.setCreateBy(null);
+            } else {
+                // 普通用户在"全部客户"时仍需要限制查看自己创建的/负责的/参与的
+                bo.setCreateBy(userId);
+                bo.setSalesPersonId(staffId != null ? staffId : -1L);
+                bo.setServiceStaffId(staffId != null ? staffId : -1L);
+            }
+        }
+        bo.setIsHighSeas("false"); // 强制为有效客户
+
+        return customerPoolService.queryCustomerListPage(bo, pageQuery);
+    }
+
+    /**
+     * 获取客户详细信息
+     */
+    @GetMapping("/{id}")
+    public R<CustomerInfoVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable("id") Long id) {
+        return R.ok(customerPoolService.queryById(id));
+    }
+
+    /**
+     * 新增客户信息
+     */
+    @Log(title = "客户管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit
+    @PostMapping
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody CustomerInfoBo bo) {
+        return toAjax(customerPoolService.insertByBo(bo));
+    }
+
+    /**
+     * 修改客户信息
+     */
+    @Log(title = "客户管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit
+    @PutMapping
+    public R<Void> edit(@RequestBody CustomerInfoBo bo) {
+        return toAjax(customerPoolService.updateByBo(bo));
+    }
+
+    /**
+     * 认领公海客户
+     */
+    @Log(title = "公海认领", businessType = BusinessType.UPDATE)
+    @PutMapping("/claimPool")
+    public R<Void> claimPool(@RequestBody CustomerClaimBo bo) {
+        return toAjax(customerPoolService.claimPool(bo));
+    }
+
+    /**
+     * 退回客户到公海池
+     */
+    @Log(title = "退回公海", businessType = BusinessType.UPDATE)
+    @PostMapping("/releaseToPool")
+    public R<Integer> releaseToPool(@RequestBody ReleaseToPoolDto bo) {
+        return R.ok(customerPoolService.releaseToPool(bo.getCustomerIds(), bo.getReason()));
+    }
+
+    /**
+     * 转移业务人员
+     */
+    @Log(title = "转移业务员", businessType = BusinessType.UPDATE)
+    @PutMapping("/transferSalesPerson")
+    public R<Integer> transferSalesPerson(@RequestBody SetCustomerInfoTagDto bo) {
+        return R.ok(customerPoolService.transferSalesPerson(bo.getCustomerIds(), bo.getSalesPersonId(), bo.getDeptId()));
+    }
+
+    /**
+     * 转移客服人员
+     */
+    @Log(title = "转移客服", businessType = BusinessType.UPDATE)
+    @PutMapping("/transferServiceStaff")
+    public R<Integer> transferServiceStaff(@RequestBody SetCustomerInfoTagDto bo) {
+        return R.ok(customerPoolService.transferServiceStaff(bo.getCustomerIds(), bo.getServiceStaffId()));
+    }
+
+    /**
+     * 获取公司下拉列表
+     */
+    @GetMapping("/companyOptionList")
+    public R<List<RemoteComCompanyVo>> companyOptionList() {
+        return R.ok(remoteComCompanyService.selectCompanyList());
+    }
+
+    /**
+     * 获取等级下拉列表
+     */
+    @GetMapping("/levelOptionList")
+    public R<List<RemoteComCustomerLevelVo>> levelOptionList() {
+        return R.ok(remoteComCustomerLevelService.selectCustomerLevelList());
+    }
+}

+ 12 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/CustomerListBo.java

@@ -38,6 +38,18 @@ public class CustomerListBo extends BaseEntity {
     /** 所属部门ID */
     private Long belongingDepartmentId;
 
+    /** 状态/合作状态 */
+    private String status;
+
+    /** 客户名称 */
+    private String customerName;
+
+    /** 企业类型ID */
+    private Long enterpriseTypeId;
+
     /** 是否为公海客户 (true: 是, false: 否) */
     private String isHighSeas;
+
+    /** 当前选中的 Tab: mine / involved / all */
+    private String activeTab;
 }

+ 6 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/SalesAnnualFinalizationBo.java

@@ -313,6 +313,12 @@ public class SalesAnnualFinalizationBo extends BaseEntity {
     @Schema(description = "批量操作ID列表")
     private List<Long> ids;
 
+    /**
+     * 成交结果
+     */
+    @Schema(description = "成交结果")
+    private Integer dealResult;
+
 }
 
 

+ 6 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/domain/bo/SalesleadsBo.java

@@ -114,6 +114,9 @@ public class SalesleadsBo extends TenantEntity {
     @Schema(description = "产品支持")
     private String productSupport;
 
+    @Schema(description = "产品支持姓名")
+    private String productSupportName;
+
 
     @Schema(description = "项目进度")
     private String projectSchedule;
@@ -132,4 +135,7 @@ public class SalesleadsBo extends TenantEntity {
 
     @Schema(description = "保留已有项目负责人(认领时使用)")
     private Boolean keepOldManager;
+
+    @Schema(description = "成交结果")
+    private Integer dealResult;
 }

+ 23 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/mapper/CustomerPoolMapper.java

@@ -0,0 +1,23 @@
+package org.dromara.customer.mapper;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.customer.domain.CustomerInfo;
+import org.dromara.customer.domain.bo.CustomerListBo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.customer.domain.vo.CustomerInfoVo;
+import org.dromara.customer.domain.vo.CustomerListVo;
+
+/**
+ * 客户公海与有效客户Mapper接口
+ *
+ * @author Antigravity
+ */
+public interface CustomerPoolMapper extends BaseMapperPlus<CustomerInfo, CustomerInfoVo> {
+
+    /**
+     * 客户列表-分页查询(包含公海与有效客户,通过 bo.isHighSeas 筛选)
+     */
+    Page<CustomerListVo> selectCustomerListPage(@Param("page") Page<CustomerListVo> page,
+                                                @Param("bo") CustomerListBo bo);
+}

+ 9 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/mapper/TeamMemberMapper.java

@@ -37,6 +37,15 @@ public interface TeamMemberMapper extends BaseMapperPlus<TeamMember, TeamMemberV
      */
     TeamMemberVo selectByObjectNoAndUserNoWithDeleted(@Param("objectNo") String objectNo, @Param("userNo") Long userNo);
 
+    /**
+     * 根据对象编号和角色编码查询团队成员(包括已删除的)
+     *
+     * @param objectNo 对象编号(如客户编号)
+     * @param roleCode 角色编码
+     * @return 团队成员信息
+     */
+    TeamMemberVo selectByObjectNoAndRoleCodeWithDeleted(@Param("objectNo") String objectNo, @Param("roleCode") String roleCode);
+
     /**
      * 恢复已删除的团队成员记录
      */

+ 59 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/ICustomerPoolService.java

@@ -0,0 +1,59 @@
+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.CustomerClaimBo;
+import org.dromara.customer.domain.bo.CustomerInfoBo;
+import org.dromara.customer.domain.bo.CustomerListBo;
+import org.dromara.customer.domain.vo.CustomerInfoVo;
+import org.dromara.customer.domain.vo.CustomerListVo;
+
+import java.util.List;
+
+/**
+ * 客户公海与有效客户Service接口
+ *
+ * @author Antigravity
+ */
+public interface ICustomerPoolService {
+
+    /**
+     * 分页查询客户列表(公海与有效客户公用)
+     */
+    TableDataInfo<CustomerListVo> queryCustomerListPage(CustomerListBo bo, PageQuery pageQuery);
+
+    /**
+     * 获取客户详细信息
+     */
+    CustomerInfoVo queryById(Long id);
+
+    /**
+     * 认领公海客户
+     */
+    Boolean claimPool(CustomerClaimBo claimBo);
+
+    /**
+     * 退回客户到公海池
+     */
+    int releaseToPool(List<Long> customerIds, String reason);
+
+    /**
+     * 转移业务人员
+     */
+    int transferSalesPerson(List<Long> customerIds, Long salesPersonId, Long deptId);
+
+    /**
+     * 转移客服人员
+     */
+    int transferServiceStaff(List<Long> customerIds, Long serviceStaffId);
+
+    /**
+     * 新增客户信息
+     */
+    Boolean insertByBo(CustomerInfoBo bo);
+
+    /**
+     * 修改客户信息
+     */
+    Boolean updateByBo(CustomerInfoBo bo);
+}

+ 169 - 79
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerInfoServiceImpl.java

@@ -35,9 +35,7 @@ import org.dromara.system.api.*;
 import org.dromara.system.api.domain.bo.RemoteUserBo;
 import org.dromara.system.api.domain.dto.AddressAreaDTO;
 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.dromara.system.api.domain.vo.RemoteUserWechatVo;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -334,7 +332,6 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
                 vo.setUpdateByName(name);
             }
         }
-
         return vo;
     }
 
@@ -406,7 +403,7 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
             .collect(Collectors.toMap(
                 CustomerSalesInfoVo::getCustomerId,
                 Function.identity(),
-                (existing, replacement) -> existing // 若有重复,保留第一个
+                (existing, replacement) -> existing
             ));
 
         // === 4. 收集人员和部门ID ===
@@ -431,13 +428,13 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
             ? Collections.emptyMap()
             : remoteErpDeptService.selectDeptNameByIds(deptIds);
 
-        // === 6. 回填人员和部门名称到 salesInfoVo ===
         for (CustomerSalesInfoVo vo : salesInfoVos) {
             vo.setSalesPerson(staffMap.get(vo.getSalesPersonId()));
             vo.setServiceStaff(staffMap.get(vo.getServiceStaffId()));
             vo.setBelongingDepartment(deptMap.get(vo.getBelongingDepartmentId()));
         }
-        //系统人员与部门
+
+        // 系统人员与部门,以及客户等级
         Set<Long> comStaffIds = new HashSet<>();
         Set<Long> comDeptIds = new HashSet<>();
 
@@ -448,15 +445,8 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
         }
 
         // === 远程调用获取名称 ===
-        Map<Long, String> comStaffMap = staffIds.isEmpty()
-            ? Collections.emptyMap()
-            : remoteComStaffService.selectStaffNameByIds(comStaffIds);
-
-        Map<Long, String> comDeptMap = deptIds.isEmpty()
-            ? Collections.emptyMap()
-            : remoteDeptService.selectDeptNameByIds(comDeptIds);
-
-        // === 回填系统人员和部门名称到 customerInfoVo ===
+        Map<Long, String> comStaffMap = staffIds.isEmpty() ? Collections.emptyMap() : remoteComStaffService.selectStaffNameByIds(comStaffIds);
+        Map<Long, String> comDeptMap = deptIds.isEmpty() ? Collections.emptyMap() : remoteDeptService.selectDeptNameByIds(comDeptIds);
         records.forEach(v -> {
             v.setSalesPersonName(comStaffMap.get(v.getSalesPersonId()));
             v.setServiceStaffName(comStaffMap.get(v.getServiceStaffId()));
@@ -865,6 +855,33 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
 
             String customerNo = entity.getCustomerNo();
 
+            // 团队成员初始化逻辑
+            Set<Long> staffIds = new HashSet<>();
+            if (entity.getSalesPersonId() != null) {
+                staffIds.add(entity.getSalesPersonId());
+            }
+            if (entity.getServiceStaffId() != null) {
+                staffIds.add(entity.getServiceStaffId());
+            }
+
+            // 只有当有相关人员时才进行远程查询和入库
+            Map<Long, String> staffNameMap = Collections.emptyMap();
+            if (!staffIds.isEmpty()) {
+                staffNameMap = remoteComStaffService.selectStaffNameByIds(staffIds);
+            }
+
+            // 使用提取的方法处理负责人
+            if (entity.getSalesPersonId() != null) {
+                saveOrUpdateTeamMember(customerNo, entity.getSalesPersonId(),
+                    staffNameMap.get(entity.getSalesPersonId()), "1", 1, 1);
+            }
+
+            // 使用提取的方法处理客服
+            if (entity.getServiceStaffId() != null) {
+                saveOrUpdateTeamMember(customerNo, entity.getServiceStaffId(),
+                    staffNameMap.get(entity.getServiceStaffId()), "3", 0, 1);
+            }
+
             /*新增客户时给每个客户创建自己的组织架构*/
             RemoteDeptVo remoteDeptVo = new RemoteDeptVo();
             remoteDeptVo.setParentId(0L);
@@ -972,6 +989,78 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
         }
     }
 
+    /**
+     * 保存或更新团队成员信息
+     * 如果成员已存在但被删除,则恢复;否则新增
+     *
+     * @param objectNo      客户编号
+     * @param userNo        人员ID
+     * @param realName      人员姓名
+     * @param roleCode      角色编码 (1:负责人, 2:客服)
+     * @param izManager     是否主管
+     * @param updateAccredit 授权更新标识
+     */
+    /**
+     * 保存或更新团队成员信息(公用方法)
+     * 确保一个客户的团队中只能有一个 roleCode=1 的负责人和一个 roleCode=2 的客服支持
+     *
+     * @param objectNo       客户编号
+     * @param userNo         人员ID
+     * @param realName       人员姓名
+     * @param roleCode       角色编码 (1:负责人, 2:客服)
+     * @param izManager      是否主管
+     * @param updateAccredit 授权更新标识
+     */
+    public void saveOrUpdateTeamMember(String objectNo, Long userNo, String realName,
+                                       String roleCode, Integer izManager, Integer updateAccredit) {
+        if (userNo == null || StringUtils.isBlank(objectNo)) {
+            return;
+        }
+
+        // 1. 查找该客户下指定角色的现有成员(包括已删除的)
+        TeamMemberVo existing = teamMemberMapper.selectByObjectNoAndRoleCodeWithDeleted(objectNo, roleCode);
+
+        if (existing != null && !existing.getUserNo().equals(userNo)) {
+            // 2. 如果存在不同的人员占据该角色,先将其软删除或更新状态
+            teamMemberMapper.update(null, new LambdaUpdateWrapper<TeamMember>()
+                .set(TeamMember::getIsDelete, 1)
+                .eq(TeamMember::getId, existing.getId()));
+        }
+
+        // 3. 查找新人员是否已在该客户团队中(任意角色)
+        TeamMemberVo existingUser = teamMemberMapper.selectByObjectNoAndUserNoWithDeleted(objectNo, userNo);
+
+        if (existingUser != null) {
+            // 4. 如果人员已存在,恢复并更新其角色信息
+            teamMemberMapper.restoreMemberById(
+                existingUser.getId(),
+                userNo,
+                realName,
+                roleCode,
+                updateAccredit,
+                izManager,
+                LoginHelper.getUserId(),
+                LoginHelper.getDeptId(),
+                PlatformContext.getPlatform()
+            );
+        } else {
+            // 5. 如果人员不存在,执行新增
+            TeamMember member = new TeamMember();
+            member.setDataType(12); // 12 代表客户类型
+            member.setObjectNo(objectNo);
+            member.setUserNo(userNo);
+            member.setRealName(realName);
+            member.setRoleCode(roleCode);
+            member.setIzManager(izManager);
+            member.setUpdateAccredit(updateAccredit);
+            member.setPlatformCode(PlatformContext.getPlatform());
+            member.setCreateUserId(LoginHelper.getUserId());
+            member.setCreateOrgId(LoginHelper.getDeptId());
+            member.setIsDelete(0);
+            teamMemberMapper.insert(member);
+        }
+    }
+
     /**
      * 修改客户信息
      *
@@ -1551,24 +1640,23 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
             return 0;
         }
 
-        if (salesPersonId == null ) {
-            log.warn("转移业务人员失败,业务人员 ID 为空");
-            throw new ServiceException("业务人员不能为空");
-        }
-
         try {
+            // 获取新业务员的姓名
+            Map<Long, String> staffMap = remoteComStaffService.selectStaffNameByIds(Collections.singleton(salesPersonId));
+            String realName = staffMap.get(salesPersonId);
+
             List<CustomerInfo> updateList = new ArrayList<>();
-            CustomerInfo customerInfo = null;
             for (Long customerId : customerIds) {
-                if (customerId == null) {
-                    continue;
-                }
+                if (customerId == null) continue;
 
-                customerInfo = baseMapper.selectById(customerId);
+                CustomerInfo customerInfo = baseMapper.selectById(customerId);
                 if (customerInfo != null) {
                     customerInfo.setSalesPersonId(salesPersonId);
                     customerInfo.setBelongingDepartmentId(deptId);
                     updateList.add(customerInfo);
+
+                    // 同步更新团队成员:角色 1 为负责人
+                    saveOrUpdateTeamMember(customerInfo.getCustomerNo(), salesPersonId, realName, "1", 1, 1);
                 } else {
                     log.warn("客户 ID: {} 不存在,跳过更新", customerId);
                 }
@@ -1581,17 +1669,12 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
                         updateList.size(), salesPersonId, deptId);
                     return updateList.size();
                 } else {
-                    log.error("批量更新客户信息失败");
                     throw new ServiceException("批量更新客户信息失败");
                 }
             }
-
-            log.info("没有需要更新的客户销售信息");
             return 0;
-
         } catch (Exception e) {
-            log.error("转移业务人员失败,客户 IDs: {}, 业务员 ID: {}, 部门 ID: {}, 错误:{}",
-                customerIds, salesPersonId, deptId, e.getMessage(), e);
+            log.error("转移业务人员失败,错误:{}", e.getMessage(), e);
             throw new ServiceException("转移业务人员失败:" + e.getMessage());
         }
     }
@@ -1599,28 +1682,26 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
     @Override
     @Transactional(rollbackFor = Exception.class)
     public int transferServiceStaff(List<Long> customerIds, Long serviceStaffId) {
-        if (CollUtil.isEmpty(customerIds)) {
-            log.warn("转移客服人员失败,客户 ID 列表为空");
+        if (CollUtil.isEmpty(customerIds) || serviceStaffId == null) {
             return 0;
         }
 
-        if (serviceStaffId == null) {
-            log.warn("转移客服人员失败,客服人员 ID 为空");
-            throw new ServiceException("客服人员和不能为空");
-        }
-
         try {
+            // 获取新客服的姓名
+            Map<Long, String> staffMap = remoteComStaffService.selectStaffNameByIds(Collections.singleton(serviceStaffId));
+            String realName = staffMap.get(serviceStaffId);
+
             List<CustomerInfo> updateList = new ArrayList<>();
-            CustomerInfo customerInfo = null;
             for (Long customerId : customerIds) {
-                if (customerId == null) {
-                    continue;
-                }
+                if (customerId == null) continue;
 
-                customerInfo = baseMapper.selectById(customerId);
+                CustomerInfo customerInfo = baseMapper.selectById(customerId);
                 if (customerInfo != null) {
                     customerInfo.setServiceStaffId(serviceStaffId);
                     updateList.add(customerInfo);
+
+                    // 同步更新团队成员:角色 3 为客服支持
+                    saveOrUpdateTeamMember(customerInfo.getCustomerNo(), serviceStaffId, realName, "3", 0, 1);
                 } else {
                     log.warn("客户 ID: {} 不存在,跳过更新", customerId);
                 }
@@ -1633,25 +1714,14 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
                         updateList.size(), serviceStaffId);
                     return updateList.size();
                 } else {
-                    log.error("批量更新客户信息失败");
                     throw new ServiceException("批量更新客户信息失败");
                 }
             }
-
-            log.info("没有需要更新的客户销售信息");
             return 0;
-
         } catch (Exception e) {
-            log.error("转移客服人员失败,客户 IDs: {}, 客服人员 ID: {},  错误:{}",
-                customerIds, serviceStaffId, e.getMessage(), e);
+            log.error("转移客服人员失败,错误:{}", e.getMessage(), e);
             throw new ServiceException("转移客服人员失败:" + e.getMessage());
         }
-
-        // 直接进行批量更新
-//        return baseMapper.update(null, new LambdaUpdateWrapper<CustomerInfo>()
-//            .set(CustomerInfo::getServiceStaffId, serviceStaffId)
-//            .in(CustomerInfo::getId, customerIds)
-//        );
     }
 
     /**
@@ -1730,37 +1800,57 @@ public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, Cus
         customer.setStatus("0"); // 认领成功后,将客户主表状态改为有效
         boolean updated = baseMapper.updateById(customer) > 0;
 
-        if (updated && claimBo.getSalesPersonId() != null) {
+        if (updated) {
             // 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);
+            if (claimBo.getSalesPersonId() != null) {
+                saveOrUpdateTeamMember(customer.getCustomerNo(), claimBo.getSalesPersonId(), "B0001", 1, 1);
+            }
+            // 3. 将客服支持加入团队成员
+            if (claimBo.getServiceStaffId() != null) {
+                saveOrUpdateTeamMember(customer.getCustomerNo(), claimBo.getServiceStaffId(), "3", 0, 0);
             }
         }
 
         return updated;
     }
 
+    private void saveOrUpdateTeamMember(String objectNo, Long userNo, String roleCode, Integer izManager, Integer updateAccredit) {
+        TeamMemberVo existing = teamMemberMapper.selectByObjectNoAndUserNoWithDeleted(objectNo, userNo);
+        String realName = "";
+        CrmStaff staff = crmStaffMapper.selectById(userNo);
+        if (staff != null) {
+            realName = staff.getStaffName();
+        }
+
+        if (existing != null) {
+            teamMemberMapper.restoreMemberById(
+                existing.getId(),
+                userNo,
+                realName,
+                roleCode,
+                updateAccredit,
+                izManager,
+                LoginHelper.getUserId(),
+                LoginHelper.getDeptId(),
+                PlatformContext.getPlatform()
+            );
+        } else {
+            TeamMember member = new TeamMember();
+            member.setDataType(12);
+            member.setObjectNo(objectNo);
+            member.setUserNo(userNo);
+            member.setRealName(realName);
+            member.setRoleCode(roleCode);
+            member.setIzManager(izManager);
+            member.setUpdateAccredit(updateAccredit);
+            member.setPlatformCode(PlatformContext.getPlatform());
+            member.setCreateUserId(LoginHelper.getUserId());
+            member.setCreateOrgId(LoginHelper.getDeptId());
+            member.setIsDelete(0);
+            teamMemberMapper.insert(member);
+        }
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public int releaseToPool(List<Long> customerIds, String reason) {

+ 247 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerPoolServiceImpl.java

@@ -0,0 +1,247 @@
+package org.dromara.customer.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+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.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.customer.domain.CustomerInfo;
+import org.dromara.customer.domain.CrmStaff;
+import org.dromara.customer.domain.TeamMember;
+import org.dromara.customer.domain.bo.CustomerClaimBo;
+import org.dromara.customer.domain.bo.CustomerInfoBo;
+import org.dromara.customer.domain.bo.CustomerListBo;
+import org.dromara.customer.domain.vo.CustomerInfoVo;
+import org.dromara.customer.domain.vo.CustomerListVo;
+import org.dromara.customer.domain.vo.TeamMemberVo;
+import org.dromara.customer.mapper.CustomerPoolMapper;
+import org.dromara.customer.mapper.TeamMemberMapper;
+import org.dromara.customer.mapper.CrmStaffMapper;
+import org.dromara.customer.service.ICustomerPoolService;
+import org.dromara.customer.service.ICustomerInfoService;
+import org.dromara.system.api.*;
+import org.dromara.system.api.domain.vo.RemoteDictDataVo;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 客户公海与有效客户Service业务层处理
+ *
+ * @author Antigravity
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class CustomerPoolServiceImpl implements ICustomerPoolService {
+
+    @DubboReference
+    private RemoteComCompanyService remoteComCompanyService;
+    @DubboReference
+    private RemoteComCustomerLevelService remoteComCustomerLevelService;
+    @DubboReference
+    private RemoteComStaffService remoteComStaffService;
+    @DubboReference
+    private RemoteDeptService remoteDeptService;
+    @DubboReference
+    private RemoteCreditLevelService remoteCreditLevelService;
+    @DubboReference
+    private RemoteDictService remoteDictService;
+
+    private final CustomerPoolMapper customerPoolMapper;
+    private final TeamMemberMapper teamMemberMapper;
+    private final CrmStaffMapper crmStaffMapper;
+    private final ICustomerInfoService customerInfoService;
+
+    @Override
+    public TableDataInfo<CustomerListVo> queryCustomerListPage(CustomerListBo bo, PageQuery pageQuery) {
+        // 强行填充默认值
+        if (StringUtils.isBlank(bo.getIsHighSeas())) {
+            Object isHighSeasObj = bo.getParams().get("isHighSeas");
+            bo.setIsHighSeas(isHighSeasObj != null ? String.valueOf(isHighSeasObj) : "false");
+        }
+
+        Page<CustomerListVo> page = customerPoolMapper.selectCustomerListPage(pageQuery.build(), bo);
+        List<CustomerListVo> records = page.getRecords();
+
+        if (CollUtil.isNotEmpty(records)) {
+            Set<Long> staffIds = new HashSet<>();
+            Set<Long> creditLevelIds = new HashSet<>();
+            Set<Long> companyIds = new HashSet<>();
+            Set<Long> customerLevelIds = new HashSet<>();
+            Set<Long> deptIds = new HashSet<>();
+
+            records.forEach(vo -> {
+                if (!"true".equals(bo.getIsHighSeas())) {
+                    if (vo.getSalesPersonId() != null) staffIds.add(vo.getSalesPersonId());
+                    if (vo.getServiceStaffId() != null) staffIds.add(vo.getServiceStaffId());
+                    if (vo.getCreditLevelId() != null) creditLevelIds.add(vo.getCreditLevelId());
+                }
+                if (vo.getBelongCompanyId() != null) companyIds.add(vo.getBelongCompanyId());
+                if (vo.getCustomerLevelId() != null) customerLevelIds.add(vo.getCustomerLevelId());
+                if (vo.getBelongingDepartmentId() != null) deptIds.add(vo.getBelongingDepartmentId());
+            });
+
+            Map<Long, String> staffMap = staffIds.isEmpty() ? Collections.emptyMap() : remoteComStaffService.selectStaffNameByIds(staffIds);
+            Map<Long, String> creditLevelMap = creditLevelIds.isEmpty() ? Collections.emptyMap() : remoteCreditLevelService.selectCreditLevelNameByIds(creditLevelIds);
+            Map<Long, String> companyMap = companyIds.isEmpty() ? Collections.emptyMap() : remoteComCompanyService.selectCompanyNameByIds(companyIds);
+            Map<Long, String> customerLevelMap = customerLevelIds.isEmpty() ? Collections.emptyMap() : remoteComCustomerLevelService.selectCustomerLevelNameByIds(customerLevelIds);
+            Map<Long, String> deptMap = deptIds.isEmpty() ? Collections.emptyMap() : remoteDeptService.selectDeptNameByIds(deptIds);
+
+            List<RemoteDictDataVo> enterpriseTypeDicts = remoteDictService.selectDictDataByType("enterprise_type");
+            Map<String, String> enterpriseTypeMap = enterpriseTypeDicts == null ? Collections.emptyMap() : enterpriseTypeDicts.stream()
+                .collect(Collectors.toMap(RemoteDictDataVo::getDictValue, RemoteDictDataVo::getDictLabel, (k1, k2) -> k1));
+
+            List<RemoteDictDataVo> cooperationDicts = remoteDictService.selectDictDataByType("cooperation_status");
+            Map<String, String> cooperationMap = cooperationDicts == null ? Collections.emptyMap() : cooperationDicts.stream()
+                .collect(Collectors.toMap(RemoteDictDataVo::getDictValue, RemoteDictDataVo::getDictLabel, (k1, k2) -> k1));
+
+            records.forEach(vo -> {
+                if (!"true".equals(bo.getIsHighSeas())) {
+                    if (StringUtils.isBlank(vo.getSalesPersonName())) {
+                        vo.setSalesPersonName(staffMap.get(vo.getSalesPersonId()));
+                    }
+                    if (StringUtils.isBlank(vo.getServiceStaffName())) {
+                        vo.setServiceStaffName(staffMap.get(vo.getServiceStaffId()));
+                    }
+                    vo.setCreditLevelName(creditLevelMap.get(vo.getCreditLevelId()));
+                }
+                vo.setCompanyName(companyMap.get(vo.getBelongCompanyId()));
+                vo.setCustomerLevelName(customerLevelMap.get(vo.getCustomerLevelId()));
+                vo.setDeptName(deptMap.get(vo.getBelongingDepartmentId()));
+
+                String typeValue = vo.getEnterpriseTypeId() != null ? String.valueOf(vo.getEnterpriseTypeId()) : null;
+                vo.setEnterpriseTypeName(enterpriseTypeMap.get(typeValue));
+                vo.setCooperationName(cooperationMap.get(vo.getStatus()));
+            });
+        }
+
+        return TableDataInfo.build(page);
+    }
+
+    @Override
+    public CustomerInfoVo queryById(Long id) {
+        return customerInfoService.queryById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean claimPool(CustomerClaimBo claimBo) {
+        Long customerId = claimBo.getId();
+        CustomerInfo customer = customerPoolMapper.selectById(customerId);
+        if (customer == null) {
+            return false;
+        }
+
+        // 1. 认领操作更新客户主表
+        customer.setSalesPersonId(claimBo.getSalesPersonId());
+        customer.setServiceStaffId(claimBo.getServiceStaffId());
+        customer.setBelongingDepartmentId(claimBo.getDeptId());
+        customer.setStatus("0"); // 认领成功后,将客户主表状态改为有效
+        boolean updated = customerPoolMapper.updateById(customer) > 0;
+
+        if (updated) {
+            // 2. 将业务员加入团队成员
+            if (claimBo.getSalesPersonId() != null) {
+                saveOrUpdateTeamMember(customer.getCustomerNo(), claimBo.getSalesPersonId(), "1", 1, 1);
+            }
+            // 3. 将客服支持加入团队成员
+            if (claimBo.getServiceStaffId() != null) {
+                saveOrUpdateTeamMember(customer.getCustomerNo(), claimBo.getServiceStaffId(), "3", 0, 0);
+            }
+        }
+
+        return updated;
+    }
+
+    private void saveOrUpdateTeamMember(String objectNo, Long userNo, String roleCode, Integer izManager, Integer updateAccredit) {
+        TeamMemberVo existing = teamMemberMapper.selectByObjectNoAndUserNoWithDeleted(objectNo, userNo);
+        String realName = "";
+        CrmStaff staff = crmStaffMapper.selectById(userNo);
+        if (staff != null) {
+            realName = staff.getStaffName();
+        }
+
+        if (existing != null) {
+            teamMemberMapper.restoreMemberById(
+                existing.getId(),
+                userNo,
+                realName,
+                roleCode,
+                updateAccredit,
+                izManager,
+                LoginHelper.getUserId(),
+                LoginHelper.getDeptId(),
+                PlatformContext.getPlatform()
+            );
+        } else {
+            TeamMember member = new TeamMember();
+            member.setDataType(12);
+            member.setObjectNo(objectNo);
+            member.setUserNo(userNo);
+            member.setRealName(realName);
+            member.setRoleCode(roleCode);
+            member.setIzManager(izManager);
+            member.setUpdateAccredit(updateAccredit);
+            member.setPlatformCode(PlatformContext.getPlatform());
+            member.setCreateUserId(LoginHelper.getUserId());
+            member.setCreateOrgId(LoginHelper.getDeptId());
+            member.setIsDelete(0);
+            teamMemberMapper.insert(member);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int releaseToPool(List<Long> customerIds, String reason) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return 0;
+        }
+
+        // 1. 获取要退回客户的 customer_no 列表
+        List<CustomerInfo> customers = customerPoolMapper.selectBatchIds(customerIds);
+        List<String> customerNos = customers.stream()
+            .map(CustomerInfo::getCustomerNo)
+            .filter(StringUtils::isNotBlank)
+            .collect(Collectors.toList());
+
+        // 2. 如果存在客户编号,则逻辑删除对应客户(dataType = 12)的所有团队成员
+        if (!customerNos.isEmpty()) {
+            teamMemberMapper.delete(new LambdaQueryWrapper<TeamMember>()
+                .eq(TeamMember::getDataType, 12)
+                .in(TeamMember::getObjectNo, customerNos)
+            );
+        }
+
+        // 3. 调用原有的 releaseToPool 方法更新客户主表状态
+        return customerInfoService.releaseToPool(customerIds, reason);
+    }
+
+    @Override
+    public int transferSalesPerson(List<Long> customerIds, Long salesPersonId, Long deptId) {
+        return customerInfoService.transferSalesPerson(customerIds, salesPersonId, deptId);
+    }
+
+    @Override
+    public int transferServiceStaff(List<Long> customerIds, Long serviceStaffId) {
+        return customerInfoService.transferServiceStaff(customerIds, serviceStaffId);
+    }
+
+    @Override
+    public Boolean insertByBo(CustomerInfoBo bo) {
+        return customerInfoService.insertByBo(bo);
+    }
+
+    @Override
+    public Boolean updateByBo(CustomerInfoBo bo) {
+        return customerInfoService.updateByBo(bo);
+    }
+}

+ 114 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/SalesAnnualFinalizationServiceImpl.java

@@ -25,9 +25,12 @@ import org.dromara.customer.service.ITeamMemberService;
 import org.dromara.customer.service.ISalesAnnualFinalizationService;
 import org.dromara.customer.controller.constant.CustomerConstants;
 import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.RemoteComStaffService;
 import org.dromara.system.api.domain.vo.RemoteUserVo;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.domain.vo.RemoteComStaffVo;
 
 
 import java.time.Duration;
@@ -55,11 +58,15 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
     @DubboReference
     private RemoteUserService remoteUserService;
 
+    @DubboReference
+    private RemoteComStaffService remoteComStaffService;
+
     /**
      * 查询年度入围项目列表
      */
     @Override
     public TableDataInfo<SalesAnnualFinalizationVo> queryPageList(SalesAnnualFinalizationBo bo, PageQuery pageQuery) {
+        applyUserPermission(bo);
         IPage<SalesAnnualFinalizationVo> result = baseMapper.selectSalesAnnualFinalizationList(pageQuery.build(), bo);
         return TableDataInfo.build(result);
     }
@@ -69,6 +76,7 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
      */
     @Override
     public List<SalesAnnualFinalizationVo> queryList(SalesAnnualFinalizationBo bo) {
+        applyUserPermission(bo);
         return baseMapper.selectSalesAnnualFinalizationList(bo);
     }
 
@@ -143,6 +151,11 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
                 memberBo.setIzManager(1);
                 teamMemberService.insertOrUpdateMember(memberBo);
             }
+
+            // 同步产品支持到团队成员表
+            if (add.getProductSupport() != null && !add.getProductSupport().isEmpty()) {
+                syncProductSupportToTeamMember(add.getId(), logDataType, add.getProductSupport());
+            }
         }
         return flag;
     }
@@ -181,6 +194,20 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
                 memberBo.setIzManager(1);
                 teamMemberService.insertOrUpdateMember(memberBo);
             }
+
+            // 同步产品支持到团队成员表
+            if (update.getProductSupport() != null && !update.getProductSupport().isEmpty()) {
+                if (oldData == null || !ObjectUtil.equals(oldData.getProductSupport(), update.getProductSupport())) {
+                    if (oldData != null && oldData.getProductSupport() != null && !oldData.getProductSupport().isEmpty()) {
+                        removeProductSupportFromTeamMember(update.getId(), logDataType, oldData.getProductSupport());
+                    }
+                    syncProductSupportToTeamMember(update.getId(), logDataType, update.getProductSupport());
+                }
+            } else {
+                if (oldData != null && oldData.getProductSupport() != null && !oldData.getProductSupport().isEmpty()) {
+                    removeProductSupportFromTeamMember(update.getId(), logDataType, oldData.getProductSupport());
+                }
+            }
         }
         return flag;
     }
@@ -275,4 +302,91 @@ public class SalesAnnualFinalizationServiceImpl implements ISalesAnnualFinalizat
         }
         return result;
     }
+
+    /**
+     * 安全转换 Long
+     */
+    private Long tryParseLong(String val) {
+        if (val == null) {
+            return null;
+        }
+        try {
+            return Long.valueOf(val.trim());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 从团队成员中安全移除产品支持
+     */
+    private void removeProductSupportFromTeamMember(Long objectNo, Integer dataType, String productSupport) {
+        if (productSupport == null || productSupport.isEmpty()) {
+            return;
+        }
+        String[] supportIdStrArray = productSupport.split(",");
+        for (String idStr : supportIdStrArray) {
+            Long id = tryParseLong(idStr);
+            if (id != null) {
+                teamMemberService.removeMember(dataType, String.valueOf(objectNo), id);
+            }
+        }
+    }
+
+    /**
+     * 同步产品支持到团队成员表(适配逗号分隔的多值)
+     */
+    private void syncProductSupportToTeamMember(Long objectNo, Integer dataType, String productSupport) {
+        if (productSupport == null || productSupport.isEmpty()) {
+            return;
+        }
+        String[] supportIdStrArray = productSupport.split(",");
+        java.util.Set<Long> supportIds = new java.util.HashSet<>();
+        for (String idStr : supportIdStrArray) {
+            Long id = tryParseLong(idStr);
+            if (id != null) {
+                supportIds.add(id);
+            }
+        }
+        if (supportIds.isEmpty()) {
+            return;
+        }
+
+        Map<Long, String> nameMap = remoteComStaffService.selectStaffNameByIds(supportIds);
+
+        for (Long id : supportIds) {
+            String realName = nameMap != null ? nameMap.get(id) : null;
+            TeamMemberBo memberBo = new TeamMemberBo();
+            memberBo.setDataType(dataType);
+            memberBo.setObjectNo(String.valueOf(objectNo));
+            memberBo.setUserNo(id);
+            memberBo.setRealName(realName);
+            memberBo.setRoleCode("3"); // 字典角色 3 代表产品支持
+            memberBo.setIzManager(0);
+            memberBo.setUpdateAccredit(0);
+            teamMemberService.insertOrUpdateMember(memberBo);
+        }
+    }
+
+    /**
+     * 应用用户权限:只允许查看自己是负责人或者产品支持的项目
+     */
+    private void applyUserPermission(SalesAnnualFinalizationBo bo) {
+        if (bo == null) {
+            return;
+        }
+        if (!LoginHelper.isSuperAdmin()) {
+            RemoteComStaffVo staffVo = remoteComStaffService.selectStaffByUserId(LoginHelper.getUserId());
+            if (bo.getParams() == null) {
+                bo.setParams(new java.util.HashMap<>());
+            }
+            bo.getParams().put("restrictToUser", true);
+            if (staffVo != null && staffVo.getStaffId() != null) {
+                bo.getParams().put("currentUserId", staffVo.getStaffId());
+            } else {
+                bo.getParams().put("currentUserId", -1L);
+            }
+        }
+    }
+
 }

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

@@ -13,6 +13,10 @@ import org.dromara.customer.domain.Salesleads;
 import org.dromara.customer.domain.bo.SalesleadsBo;
 import org.dromara.customer.domain.bo.TeamMemberBo;
 import org.dromara.customer.domain.vo.SalesleadsVo;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.system.api.RemoteComStaffService;
+import org.dromara.system.api.domain.vo.RemoteComStaffVo;
+import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.customer.mapper.SalesleadsMapper;
 import org.dromara.customer.service.IOperationLogService;
 import org.dromara.customer.service.ISalesleadsService;
@@ -23,6 +27,8 @@ import org.springframework.transaction.annotation.Transactional;
 import java.time.Duration;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * 销售线索/项目商机 Service 业务层处理
@@ -40,6 +46,9 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
     private final ITeamMemberService teamMemberService;
     private final IOperationLogService operationLogService;
 
+    @DubboReference
+    private RemoteComStaffService remoteComStaffService;
+
     /**
      * 查询
      */
@@ -47,6 +56,7 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
     public SalesleadsVo queryById(Long id) {
         SalesleadsBo bo = new SalesleadsBo();
         bo.setId(id);
+        applyUserPermission(bo);
         List<SalesleadsVo> list = baseMapper.selectSalesleadsList(bo);
         return list.isEmpty() ? null : list.get(0);
     }
@@ -56,6 +66,7 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
      */
     @Override
     public TableDataInfo<SalesleadsVo> queryPageList(SalesleadsBo bo, PageQuery pageQuery) {
+        applyUserPermission(bo);
         IPage<SalesleadsVo> result = baseMapper.selectSalesleadsList(pageQuery.build(), bo);
         return TableDataInfo.build(result);
     }
@@ -65,6 +76,7 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
      */
     @Override
     public List<SalesleadsVo> queryList(SalesleadsBo bo) {
+        applyUserPermission(bo);
         return baseMapper.selectSalesleadsList(bo);
     }
 
@@ -91,6 +103,10 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
                 // 新增成功后,将负责人同步到团队成员表
                 syncLeaderToTeamMember(add.getId(), add.getLeader(), add.getLeaderName());
             }
+            if (add.getId() != null && bo.getProductSupport() != null && !bo.getProductSupport().isEmpty()) {
+                // 新增成功后,将产品支持同步到团队成员表
+                syncProductSupportToTeamMember(add.getId(), bo.getProductSupport(), bo.getProductSupportName());
+            }
             // 记录日志
             operationLogService.recordLog(CustomerConstants.DATA_TYPE_LEADS, String.valueOf(add.getId()), CustomerConstants.ACTION_TYPE_INSERT, null, "创建了销售线索", add.getProjectName());
         }
@@ -131,6 +147,22 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
                 syncLeaderToTeamMember(bo.getId(), bo.getLeader(), bo.getLeaderName());
             }
         }
+        if (success) {
+            if (bo.getProductSupport() != null && !bo.getProductSupport().isEmpty()) {
+                if (oldData == null || !ObjectUtils.equals(oldData.getProductSupport(), bo.getProductSupport())) {
+                    // 如果旧产品支持存在,则从团队成员中移除
+                    if (oldData != null && oldData.getProductSupport() != null && !oldData.getProductSupport().isEmpty()) {
+                        removeProductSupportFromTeamMember(bo.getId(), oldData.getProductSupport());
+                    }
+                    syncProductSupportToTeamMember(bo.getId(), bo.getProductSupport(), bo.getProductSupportName());
+                }
+            } else {
+                // 如果新产品支持为空,但旧产品支持存在,则从团队中移除
+                if (oldData != null && oldData.getProductSupport() != null && !oldData.getProductSupport().isEmpty()) {
+                    removeProductSupportFromTeamMember(bo.getId(), oldData.getProductSupport());
+                }
+            }
+        }
         return success;
     }
 
@@ -161,6 +193,7 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
             update.setId(id);
             update.setLeader(bo.getLeader());
             update.setLeaderName(bo.getLeaderName());
+            update.setProductSupport(bo.getProductSupport());
             update.setIzClue(0); // 认领后转为项目商机
             update.setProjectType(CustomerConstants.PROJECT_TYPE_OPPORTUNITY);
             update.setStatus("0"); // 转为商机后,状态设为跟进中
@@ -179,6 +212,21 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
                 }
 
                 syncLeaderToTeamMember(id, bo.getLeader(), bo.getLeaderName());
+                
+                // 处理产品支持同步逻辑
+                if (bo.getProductSupport() != null && !bo.getProductSupport().isEmpty()) {
+                    if (oldData == null || !ObjectUtils.equals(oldData.getProductSupport(), bo.getProductSupport())) {
+                        if (oldData != null && oldData.getProductSupport() != null && !oldData.getProductSupport().isEmpty()) {
+                            removeProductSupportFromTeamMember(id, oldData.getProductSupport());
+                        }
+                        syncProductSupportToTeamMember(id, bo.getProductSupport(), bo.getProductSupportName());
+                    }
+                } else {
+                    if (oldData != null && oldData.getProductSupport() != null && !oldData.getProductSupport().isEmpty()) {
+                        removeProductSupportFromTeamMember(id, oldData.getProductSupport());
+                    }
+                }
+                
                 // 记录日志
                 operationLogService.recordLog(CustomerConstants.DATA_TYPE_LEADS, String.valueOf(id), CustomerConstants.ACTION_TYPE_CLAIM, null, "认领了销售线索", bo.getLeaderName());
             }
@@ -247,17 +295,105 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
      */
     private void syncLeaderToTeamMember(Long objectNo, Long leaderId, String leaderName) {
         if (leaderId == null) return;
+        String realName = leaderName;
+        if (realName == null || realName.isEmpty()) {
+            Map<Long, String> nameMap = remoteComStaffService.selectStaffNameByIds(Set.of(leaderId));
+            if (nameMap != null && nameMap.containsKey(leaderId)) {
+                realName = nameMap.get(leaderId);
+            }
+        }
         TeamMemberBo memberBo = new TeamMemberBo();
         memberBo.setDataType(CustomerConstants.DATA_TYPE_LEADS);
         memberBo.setObjectNo(String.valueOf(objectNo));
         memberBo.setUserNo(leaderId);
-        memberBo.setRealName(leaderName);
+        memberBo.setRealName(realName);
         memberBo.setRoleCode(CustomerConstants.TEAM_ROLE_LEADER); // 业务负责人的字典值
         memberBo.setIzManager(1);
         memberBo.setUpdateAccredit(1);
         teamMemberService.insertOrUpdateMember(memberBo);
     }
 
+    /**
+     * 安全转换 Long
+     */
+    private Long tryParseLong(String val) {
+        if (val == null) {
+            return null;
+        }
+        try {
+            return Long.valueOf(val.trim());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 从团队成员中安全移除产品支持
+     */
+    private void removeProductSupportFromTeamMember(Long objectNo, String productSupport) {
+        if (productSupport == null || productSupport.isEmpty()) {
+            return;
+        }
+        String[] supportIdStrArray = productSupport.split(",");
+        for (String idStr : supportIdStrArray) {
+            Long id = tryParseLong(idStr);
+            if (id != null) {
+                teamMemberService.removeMember(CustomerConstants.DATA_TYPE_LEADS, String.valueOf(objectNo), id);
+            }
+        }
+    }
+
+    /**
+     * 同步产品支持到团队成员表(适配逗号分隔的多值)
+     */
+    private void syncProductSupportToTeamMember(Long objectNo, String productSupport, String supportName) {
+        if (productSupport == null || productSupport.isEmpty()) {
+            return;
+        }
+        String[] supportIdStrArray = productSupport.split(",");
+        Set<Long> supportIds = new java.util.HashSet<>();
+        for (String idStr : supportIdStrArray) {
+            Long id = tryParseLong(idStr);
+            if (id != null) {
+                supportIds.add(id);
+            }
+        }
+        if (supportIds.isEmpty()) {
+            return;
+        }
+
+        Map<Long, String> nameMap = remoteComStaffService.selectStaffNameByIds(supportIds);
+        String[] supportNameArray = null;
+        if (supportName != null && !supportName.isEmpty()) {
+            supportNameArray = supportName.split(",");
+        }
+
+        int index = 0;
+        for (String idStr : supportIdStrArray) {
+            Long id = tryParseLong(idStr);
+            if (id != null) {
+                String realName = nameMap != null ? nameMap.get(id) : null;
+                if (realName == null || realName.isEmpty()) {
+                    if (supportNameArray != null && index < supportNameArray.length) {
+                        realName = supportNameArray[index].trim();
+                    } else {
+                        realName = supportName;
+                    }
+                }
+                TeamMemberBo memberBo = new TeamMemberBo();
+                memberBo.setDataType(CustomerConstants.DATA_TYPE_LEADS);
+                memberBo.setObjectNo(String.valueOf(objectNo));
+                memberBo.setUserNo(id);
+                memberBo.setRealName(realName);
+                memberBo.setRoleCode("3"); 
+                memberBo.setIzManager(0);
+                memberBo.setUpdateAccredit(0);
+                teamMemberService.insertOrUpdateMember(memberBo);
+            }
+            index++;
+        }
+    }
+
     /**
      * 获取项目进度名称
      */
@@ -272,4 +408,25 @@ public class SalesleadsServiceImpl implements ISalesleadsService {
         };
     }
 
+    /**
+     * 应用用户权限:只允许查看自己是负责人或者产品支持的项目
+     */
+    private void applyUserPermission(SalesleadsBo bo) {
+        if (bo == null) {
+            return;
+        }
+        if (!LoginHelper.isSuperAdmin()) {
+            RemoteComStaffVo staffVo = remoteComStaffService.selectStaffByUserId(LoginHelper.getUserId());
+            if (bo.getParams() == null) {
+                bo.setParams(new java.util.HashMap<>());
+            }
+            bo.getParams().put("restrictToUser", true);
+            if (staffVo != null && staffVo.getStaffId() != null) {
+                bo.getParams().put("currentUserId", staffVo.getStaffId());
+            } else {
+                bo.getParams().put("currentUserId", -1L);
+            }
+        }
+    }
+
 }

+ 31 - 1
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/TeamMemberServiceImpl.java

@@ -2,6 +2,7 @@ package org.dromara.customer.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.customer.controller.constant.CustomerConstants;
@@ -80,7 +81,6 @@ public class TeamMemberServiceImpl implements ITeamMemberService {
             }
         }
         
-        // 翻译权限 (team_permission)
         if (vo.getUpdateAccredit() != null) {
             List<RemoteDictDataVo> permDicts = remoteDictService.selectDictDataByType("team_permission");
             if (permDicts != null) {
@@ -158,6 +158,20 @@ public class TeamMemberServiceImpl implements ITeamMemberService {
                 }
             }
         }
+        if (bo.getId() != null) {
+            TeamMemberVo member = baseMapper.selectVoById(bo.getId());
+            if (member != null) {
+                if (Integer.valueOf(12).equals(member.getDataType())) {
+                    if ("1".equals(member.getRoleCode()) || "B0001".equals(member.getRoleCode()) || "3".equals(member.getRoleCode())) {
+                        throw new ServiceException("业务负责人和客服支持是不可编辑的");
+                    }
+                } else if (Integer.valueOf(1).equals(member.getDataType()) || Integer.valueOf(2).equals(member.getDataType())) {
+                    if ("1".equals(member.getRoleCode()) || "3".equals(member.getRoleCode())) {
+                        throw new ServiceException("项目负责人、产品支持和客服支持是不可编辑的");
+                    }
+                }
+            }
+        }
         TeamMember update = MapstructUtils.convert(bo, TeamMember.class);
         boolean flag = baseMapper.updateById(update) > 0;
         if (flag) {
@@ -178,6 +192,17 @@ public class TeamMemberServiceImpl implements ITeamMemberService {
             if (members == null || members.isEmpty()) {
                 return false;
             }
+            for (TeamMemberVo member : members) {
+                if (Integer.valueOf(12).equals(member.getDataType())) {
+                    if ("1".equals(member.getRoleCode()) || "B0001".equals(member.getRoleCode()) || "3".equals(member.getRoleCode())) {
+                        throw new ServiceException("业务负责人和客服支持是不可删除的");
+                    }
+                } else if (Integer.valueOf(1).equals(member.getDataType()) || Integer.valueOf(2).equals(member.getDataType())) {
+                    if ("1".equals(member.getRoleCode()) || "3".equals(member.getRoleCode())) {
+                        throw new ServiceException("项目负责人、产品支持和客服支持是不可删除的");
+                    }
+                }
+            }
             boolean flag = baseMapper.deleteByIds(ids) > 0;
             if (flag) {
                 for (TeamMemberVo member : members) {
@@ -231,6 +256,11 @@ public class TeamMemberServiceImpl implements ITeamMemberService {
     public Boolean removeMember(Integer dataType, String objectNo, Long userNo) {
         TeamMemberVo existing = baseMapper.selectByObjectNoAndUserNo(objectNo, userNo);
         if (existing != null) {
+            if (Integer.valueOf(12).equals(existing.getDataType())) {
+                if ("1".equals(existing.getRoleCode()) || "B0001".equals(existing.getRoleCode()) || "3".equals(existing.getRoleCode())) {
+                    throw new ServiceException("业务负责人和客服支持是不可删除的");
+                }
+            }
             boolean flag = baseMapper.deleteById(existing.getId()) > 0;
             if (flag) {
                 operationLogService.recordLog(dataType, objectNo, CustomerConstants.ACTION_TYPE_DELETE, null, CustomerConstants.MODULE_TEAM_MEMBER, existing.getRealName());

+ 26 - 11
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/WorkbenchServiceImpl.java

@@ -89,7 +89,7 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
         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));
+            .eq(SalesResultAnalyze::getDealResult, 1).ge(SalesResultAnalyze::getCreateTime, monthStart).and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete)));
 
         // 7天前值
         long totalOld = countBefore(weekAgo, "0", false);
@@ -151,11 +151,11 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
             win = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
                 .eq(SalesResultAnalyze::getDealResult, 1)
                 .in(SalesResultAnalyze::getObjectNo, oppNos)
-                .eq(SalesResultAnalyze::getIsDelete, 0));
+                .and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete)));
             lose = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
                 .eq(SalesResultAnalyze::getDealResult, 2)
                 .in(SalesResultAnalyze::getObjectNo, oppNos)
-                .eq(SalesResultAnalyze::getIsDelete, 0));
+                .and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete)));
         }
 
         // 7天前
@@ -167,12 +167,12 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
             oldWin = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
                 .eq(SalesResultAnalyze::getDealResult, 1)
                 .in(SalesResultAnalyze::getObjectNo, oppNos)
-                .eq(SalesResultAnalyze::getIsDelete, 0)
+                .and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete))
                 .le(SalesResultAnalyze::getCreateTime, weekAgo));
             oldLose = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
                 .eq(SalesResultAnalyze::getDealResult, 2)
                 .in(SalesResultAnalyze::getObjectNo, oppNos)
-                .eq(SalesResultAnalyze::getIsDelete, 0)
+                .and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete))
                 .le(SalesResultAnalyze::getCreateTime, weekAgo));
         }
 
@@ -219,11 +219,11 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
             win = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
                 .eq(SalesResultAnalyze::getDealResult, 1)
                 .in(SalesResultAnalyze::getObjectNo, selectionNos)
-                .eq(SalesResultAnalyze::getIsDelete, 0));
+                .and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete)));
             lose = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
                 .eq(SalesResultAnalyze::getDealResult, 2)
                 .in(SalesResultAnalyze::getObjectNo, selectionNos)
-                .eq(SalesResultAnalyze::getIsDelete, 0));
+                .and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete)));
         }
 
         List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
@@ -252,12 +252,27 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
     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<SalesResultAnalyze> wins = salesresultanalyzeMapper.selectList(new LambdaQueryWrapper<SalesResultAnalyze>()
+            .eq(SalesResultAnalyze::getDealResult, 1)
+            .eq(SalesResultAnalyze::getDataType, 2)
+            .and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete)));
+            
+        long winCount = wins.size();
+        BigDecimal winAmount = BigDecimal.ZERO;
+        
+        if (!wins.isEmpty()) {
+            List<String> winNos = wins.stream().map(SalesResultAnalyze::getObjectNo).filter(Objects::nonNull).collect(Collectors.toList());
+            if (!winNos.isEmpty()) {
+                winAmount = salesleadsMapper.selectList(new LambdaQueryWrapper<Salesleads>().in(Salesleads::getProjectNo, winNos).eq(Salesleads::getDelFlag, "0"))
+                    .stream().map(Salesleads::getProjectBudget).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+            }
+        }
         
         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 + "个")); // 赢单金额需关联
+        stats.add(new WorkbenchStatVo.StatItem("赢单", winAmount.setScale(0, BigDecimal.ROUND_HALF_UP).toString(), winCount + "个"));
         return stats;
     }
 
@@ -274,7 +289,7 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
         
         // 已成交客户:在成交结果表中有记录的
         long deal = salesresultanalyzeMapper.selectCount(new LambdaQueryWrapper<SalesResultAnalyze>()
-            .eq(SalesResultAnalyze::getDealResult, 1).eq(SalesResultAnalyze::getIsDelete, 0));
+            .eq(SalesResultAnalyze::getDealResult, 1).and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete)));
 
         List<WorkbenchStatVo.StatItem> stats = new ArrayList<>();
         stats.add(new WorkbenchStatVo.StatItem("公海客户", String.valueOf(publicPool), "个"));
@@ -296,7 +311,7 @@ public class WorkbenchServiceImpl implements IWorkbenchService {
         List<SalesResultAnalyze> wins = salesresultanalyzeMapper.selectList(new LambdaQueryWrapper<SalesResultAnalyze>()
             .eq(SalesResultAnalyze::getDealResult, 1)
             .ge(SalesResultAnalyze::getCreateTime, monthStart)
-            .eq(SalesResultAnalyze::getIsDelete, 0));
+            .and(w -> w.eq(SalesResultAnalyze::getIsDelete, 0).or().isNull(SalesResultAnalyze::getIsDelete)));
         
         BigDecimal winAmount = BigDecimal.ZERO;
         if (!wins.isEmpty()) {

+ 9 - 0
ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/CustomerInfoMapper.xml

@@ -113,6 +113,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="bo.belongingDepartmentId != null">
                 AND ci.belonging_department_id = #{bo.belongingDepartmentId}
             </if>
+            <if test="bo.status != null and bo.status != ''">
+                AND ci.status = #{bo.status}
+            </if>
+            <if test="bo.customerName != null and bo.customerName != ''">
+                AND (ci.customer_name LIKE CONCAT('%', #{bo.customerName}, '%') OR ci.business_customer_name LIKE CONCAT('%', #{bo.customerName}, '%'))
+            </if>
+            <if test="bo.enterpriseTypeId != null">
+                AND ci.customer_type_id = #{bo.enterpriseTypeId}
+            </if>
         </where>
         ORDER BY ci.id DESC
     </select>

+ 109 - 0
ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/CustomerPoolMapper.xml

@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.dromara.customer.mapper.CustomerPoolMapper">
+
+    <!-- 客户列表-分页查询(包含公海与有效客户,通过 bo.isHighSeas 筛选) -->
+    <select id="selectCustomerListPage" resultType="org.dromara.customer.domain.vo.CustomerListVo">
+        SELECT
+            ci.id,
+            ci.customer_no AS customerNo,
+            IFNULL(ci.customer_name, ci.business_customer_name) AS customerName,
+            ci.industry_category_id AS industryCategoryId,
+            ic.industry_category_name AS industryName,
+            csi.credit_amount AS creditAmount,
+            csi.credit_management_id AS creditLevelId,
+            csi.accounts_receivable AS accountsReceivable,
+            ci.sales_person_id AS salesPersonId,
+            sp.staff_name AS salesPersonName,
+            ci.belong_company_id AS belongCompanyId,
+            ci.customer_type_id AS enterpriseTypeId,
+            ci.customer_level_id AS customerLevelId,
+            ci.service_staff_id AS serviceStaffId,
+            ss.staff_name AS serviceStaffName,
+            ci.belonging_department_id AS belongingDepartmentId,
+            sd.dept_name AS deptName,
+            ci.status
+        FROM customer_info ci
+        LEFT JOIN customer_sales_info csi ON ci.id = csi.customer_id AND csi.del_flag = '0'
+        LEFT JOIN industry_category ic ON ci.industry_category_id = ic.id AND ic.del_flag = '0'
+        LEFT JOIN com_staff sp ON ci.sales_person_id = sp.staff_id AND sp.del_flag = '0'
+        LEFT JOIN com_staff ss ON ci.service_staff_id = ss.staff_id AND ss.del_flag = '0'
+        LEFT JOIN sys_dept sd ON sp.dept_id = sd.dept_id
+        <where>
+            ci.del_flag = '0'
+            <choose>
+                <when test="bo.isHighSeas == 'true'">
+                    AND (ci.sales_person_id IS NULL OR ci.sales_person_id = 0)
+                    AND (ci.service_staff_id IS NULL OR ci.service_staff_id = 0)
+                </when>
+                <otherwise>
+                    AND ( (ci.sales_person_id IS NOT NULL AND ci.sales_person_id != 0) 
+                       OR (ci.service_staff_id IS NOT NULL AND ci.service_staff_id != 0) )
+                    <if test="bo.activeTab == 'mine' and bo.salesPersonId != null">
+                        AND ci.sales_person_id = #{bo.salesPersonId}
+                    </if>
+                    <if test="bo.activeTab == 'involved' and bo.serviceStaffId != null">
+                        AND (ci.sales_person_id IS NULL OR ci.sales_person_id != #{bo.serviceStaffId})
+                        AND (ci.service_staff_id IS NULL OR ci.service_staff_id != #{bo.serviceStaffId})
+                        AND ci.customer_no IN (
+                            SELECT tm.object_no FROM team_member tm
+                            WHERE tm.user_no = #{bo.serviceStaffId}
+                              AND tm.data_type = 12
+                              AND tm.is_delete = 0
+                        )
+                    </if>
+                    <if test="(bo.activeTab == null or bo.activeTab == 'all' or bo.activeTab == '') and bo.params.isAdmin == 'false'">
+                        AND (
+                            ci.create_by = #{bo.params.createBy}
+                            OR ci.sales_person_id = #{bo.salesPersonId}
+                            OR ci.service_staff_id = #{bo.serviceStaffId}
+                            OR ci.customer_no IN (
+                                SELECT tm.object_no FROM team_member tm 
+                                WHERE tm.user_no = #{bo.serviceStaffId} 
+                                  AND tm.data_type = 12 
+                                  AND tm.is_delete = 0
+                            )
+                        )
+                    </if>
+                </otherwise>
+            </choose>
+            <if test="bo.platformCode != null and bo.platformCode != ''">
+                AND ci.platform_code = #{bo.platformCode}
+            </if>
+            <if test="bo.id != null">
+                AND ci.id = #{bo.id}
+            </if>
+            <if test="bo.customerNo != null and bo.customerNo != ''">
+                AND ci.customer_no LIKE CONCAT('%', #{bo.customerNo}, '%')
+            </if>
+            <if test="bo.industryCategoryId != null">
+                AND ci.industry_category_id = #{bo.industryCategoryId}
+            </if>
+            <if test="bo.customerLevelId != null">
+                AND ci.customer_level_id = #{bo.customerLevelId}
+            </if>
+            <if test="bo.salesPersonId != null and bo.activeTab != 'mine' and (bo.activeTab != 'all' or bo.params.isAdmin == 'true')">
+                AND ci.sales_person_id = #{bo.salesPersonId}
+            </if>
+            <if test="bo.serviceStaffId != null and bo.activeTab != 'involved' and (bo.activeTab != 'all' or bo.params.isAdmin == 'true')">
+                AND ci.service_staff_id = #{bo.serviceStaffId}
+            </if>
+            <if test="bo.belongingDepartmentId != null">
+                AND ci.belonging_department_id = #{bo.belongingDepartmentId}
+            </if>
+            <if test="bo.status != null and bo.status != ''">
+                AND ci.status = #{bo.status}
+            </if>
+            <if test="bo.customerName != null and bo.customerName != ''">
+                AND (ci.customer_name LIKE CONCAT('%', #{bo.customerName}, '%') OR ci.business_customer_name LIKE CONCAT('%', #{bo.customerName}, '%'))
+            </if>
+            <if test="bo.enterpriseTypeId != null">
+                AND ci.customer_type_id = #{bo.enterpriseTypeId}
+            </if>
+        </where>
+        ORDER BY ci.create_time DESC
+    </select>
+
+</mapper>

+ 11 - 0
ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/SalesAnnualFinalizationMapper.xml

@@ -43,6 +43,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="bo.productSupport != null and bo.productSupport != ''">
                 AND s.product_support = #{bo.productSupport}
             </if>
+            <if test="bo.dealResult != null">
+                AND EXISTS (
+                    SELECT 1 FROM salesresultanalyze sra 
+                    WHERE sra.objectNo COLLATE utf8mb4_unicode_ci = s.project_no COLLATE utf8mb4_unicode_ci 
+                    AND sra.dealResult = #{bo.dealResult}
+                    AND (sra.IsDelete = 0 OR sra.IsDelete IS NULL)
+                )
+            </if>
+            <if test="bo.params.restrictToUser == true">
+                AND (s.leader = #{bo.params.currentUserId} OR FIND_IN_SET(#{bo.params.currentUserId}, s.product_support) > 0)
+            </if>
             ${bo.params.dataScope}
         </where>
         ORDER BY s.create_time DESC

+ 6 - 0
ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/SalesleadsMapper.xml

@@ -45,6 +45,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="bo.deptNo != null and bo.deptNo != ''">
                 AND cs.dept_id = #{bo.deptNo}
             </if>
+            <if test="bo.dealResult != null">
+                AND sra.dealResult = #{bo.dealResult}
+            </if>
+            <if test="bo.params.restrictToUser == true">
+                AND (s.leader = #{bo.params.currentUserId} OR FIND_IN_SET(#{bo.params.currentUserId}, s.product_support) > 0)
+            </if>
             ${bo.params.dataScope}
         </where>
         ORDER BY create_time DESC

+ 7 - 0
ruoyi-modules/ruoyi-customer/src/main/resources/mapper/customer/TeamMemberMapper.xml

@@ -29,6 +29,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         LIMIT 1
     </select>
 
+    <select id="selectByObjectNoAndRoleCodeWithDeleted" resultType="org.dromara.customer.domain.vo.TeamMemberVo">
+        SELECT * FROM team_member
+        WHERE object_no = #{objectNo} AND role_code = #{roleCode}
+        ORDER BY is_delete ASC, update_time DESC
+        LIMIT 1
+    </select>
+
     <update id="restoreMemberById">
         UPDATE team_member SET 
             is_delete = 0,