Browse Source

refactor(lead/opportunity): ConvertibleLeadCatalogPort 收窄跨域契约(架构审查候选2)

- crm-lead 新增 port/inbound:ConvertibleLeadCatalogPort(单方法)+
  ConvertibleLeadView(随端口同包)+ ConvertibleLeadCatalogPortImpl
  (逻辑自 LeadServiceImpl 原样迁入,票 12 口径不变)
- 删 ILeadService.listConvertibleLeads + LeadServiceImpl 实现 +
  domain/dto/ConvertibleLeadView + LeadConvertibleQueryTest
  (宽服务接口不再挂为外域服务的方法,商机侧不再 import ILeadService)
- 商机侧 4 处改注入端口:CreateServiceImpl / IOpportunityCreateService /
  CreateController / 测试;占用过滤(查 opportunity.source_lead_id)留商机侧
- 验证:crm-lead 119 + crm-opportunity 165 全绿
master
luoweijian 2 weeks ago
parent
commit
83299113e7
  1. 26
      crm-lead/src/main/java/com/crm/lead/port/inbound/ConvertibleLeadCatalogPort.java
  2. 8
      crm-lead/src/main/java/com/crm/lead/port/inbound/ConvertibleLeadView.java
  3. 36
      crm-lead/src/main/java/com/crm/lead/port/inbound/impl/ConvertibleLeadCatalogPortImpl.java
  4. 10
      crm-lead/src/main/java/com/crm/lead/service/ILeadService.java
  5. 11
      crm-lead/src/main/java/com/crm/lead/service/impl/LeadServiceImpl.java
  6. 33
      crm-lead/src/test/java/com/crm/lead/port/inbound/ConvertibleLeadCatalogPortImplTest.java
  7. 2
      crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityCreateController.java
  8. 12
      crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityCreateService.java
  9. 14
      crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCreateServiceImpl.java
  10. 11
      crm-opportunity/src/test/java/com/crm/opportunity/service/impl/OpportunityCreateServiceImplTest.java

26
crm-lead/src/main/java/com/crm/lead/port/inbound/ConvertibleLeadCatalogPort.java

@ -0,0 +1,26 @@
package com.crm.lead.port.inbound;
import java.util.List;
/**
* 可转商机候选线索目录入站端口 12 数据源 / Candidate #2商机侧消费的窄查询契约
*
* <p>商机侧新建入口oppSource=线索转入关联线索下拉数据源此前商机侧 import
* 线索域宽服务接口 {@code ILeadService}20+ 方法仅为此一处查询本端口把契约收窄到
* 单方法宽接口退回线索域内部ADR-0031对称 {@code port/outbound}
* {@code OpportunityCreationPort}出站契约 lead 声明他域实现入站契约本域实现他域调用</p>
*
* <p>职责边界 ADR-0025准入 = 归属用户名下 {@code status IN (已领取 3 / 跟进中 4)}
* 与转商机前置对称线索域判定未被商机占用过滤由商机侧补本端口不知
* opportunity </p>
*/
public interface ConvertibleLeadCatalogPort {
/**
* 列出该用户可关联到商机的候选线索
*
* @param ownerUserId 归属用户 id商机侧传当前登录人
* @return 轻量投影id/名称/电话无候选时返回空列表
*/
List<ConvertibleLeadView> listConvertibleLeads(Long ownerUserId);
}

8
crm-lead/src/main/java/com/crm/lead/domain/dto/ConvertibleLeadView.java → crm-lead/src/main/java/com/crm/lead/port/inbound/ConvertibleLeadView.java

@ -1,11 +1,13 @@
package com.crm.lead.domain.dto; package com.crm.lead.port.inbound;
/** /**
* 可转商机候选线索视图 12商机侧新建入口关联线索下拉数据源 * 可转商机候选线索视图 12商机侧新建入口关联线索下拉数据源
* *
* <p>轻量投影仅够下拉展示 + 回填线索 id + 名称 + 电话候选准入 * <p>轻量投影仅够下拉展示 + 回填线索 id + 名称 + 电话候选准入
* status IN 已领取/跟进中 {@code ILeadService.listConvertibleLeads} 在线索域判定 * status IN 已领取/跟进中 {@link ConvertibleLeadCatalogPort} 在线索域判定
* 未被商机占用过滤由商机侧补 opportunity.source_lead_id</p> * 未被商机占用过滤由商机侧补 opportunity.source_lead_id
* 随端口同包ADR-0031对称 port/outbound 契约 cmd/exception 同包先例
* 本包类型是线索域对商机侧发布的契约面商机侧不得 import 线索域 service/domain 内部包</p>
* *
* @param id 线索 id回填 source_lead_id * @param id 线索 id回填 source_lead_id
* @param leadName 线索名称下拉展示 * @param leadName 线索名称下拉展示

36
crm-lead/src/main/java/com/crm/lead/port/inbound/impl/ConvertibleLeadCatalogPortImpl.java

@ -0,0 +1,36 @@
package com.crm.lead.port.inbound.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.crm.lead.constant.LeadConstants;
import com.crm.lead.domain.entity.Lead;
import com.crm.lead.mapper.LeadMapper;
import com.crm.lead.port.inbound.ConvertibleLeadCatalogPort;
import com.crm.lead.port.inbound.ConvertibleLeadView;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 可转商机候选线索目录端口实现逻辑自 {@code LeadServiceImpl.listConvertibleLeads}
* 原样迁入 12 口径不变归属 + status IN (已领取 3 / 跟进中 4)
*
* <p>窄查询契约的实现ADR-0031直查 {@code LeadMapper}不复用宽服务接口
* 未被商机占用过滤留商机侧本实现不知 opportunity </p>
*/
@Service
@RequiredArgsConstructor
public class ConvertibleLeadCatalogPortImpl implements ConvertibleLeadCatalogPort {
private final LeadMapper leadMapper;
@Override
public List<ConvertibleLeadView> listConvertibleLeads(Long ownerUserId) {
List<Lead> leads = leadMapper.selectList(new LambdaQueryWrapper<Lead>()
.eq(Lead::getOwnerUserId, ownerUserId)
.in(Lead::getStatus, LeadConstants.STATUS_CLAIMED, LeadConstants.STATUS_FOLLOWING));
return leads.stream()
.map(l -> new ConvertibleLeadView(l.getId(), l.getLeadName(), l.getPhone()))
.toList();
}
}

10
crm-lead/src/main/java/com/crm/lead/service/ILeadService.java

@ -2,7 +2,6 @@ package com.crm.lead.service;
import com.crm.base.domain.result.PageResult; import com.crm.base.domain.result.PageResult;
import com.crm.base.domain.result.BatchResult; import com.crm.base.domain.result.BatchResult;
import com.crm.lead.domain.dto.ConvertibleLeadView;
import com.crm.lead.domain.dto.LeadDTO; import com.crm.lead.domain.dto.LeadDTO;
import com.crm.lead.domain.dto.LeadBatchFailItem; import com.crm.lead.domain.dto.LeadBatchFailItem;
import com.crm.lead.domain.dto.LeadFeedbackDTO; import com.crm.lead.domain.dto.LeadFeedbackDTO;
@ -34,15 +33,6 @@ public interface ILeadService {
/** 视图统计卡片(与 {@link #pageLeads} 同一套筛选口径,口径随视图,ADR-0023) */ /** 视图统计卡片(与 {@link #pageLeads} 同一套筛选口径,口径随视图,ADR-0023) */
LeadStatsDTO countStats(LeadPageParam param); LeadStatsDTO countStats(LeadPageParam param);
/**
* 可转商机候选线索 12商机侧新建入口关联线索下拉数据源
* <p>准入 = 该用户名下 {@code status IN (已领取 3 / 跟进中 4)} 的线索对齐转商机准入
* 未被商机占用过滤由商机侧补本方法不知 opportunity </p>
*
* @param ownerUserId 归属用户 id商机侧传当前登录人
*/
List<ConvertibleLeadView> listConvertibleLeads(Long ownerUserId);
// ==================== 创建/编辑/删除 ==================== // ==================== 创建/编辑/删除 ====================
/** /**

11
crm-lead/src/main/java/com/crm/lead/service/impl/LeadServiceImpl.java

@ -14,7 +14,6 @@ import com.crm.lead.creation.LeadCreateContext;
import com.crm.lead.creation.LeadCreationPlanner; import com.crm.lead.creation.LeadCreationPlanner;
import com.crm.lead.deadline.DeadlineBudget; import com.crm.lead.deadline.DeadlineBudget;
import com.crm.lead.deadline.LeadDeadlines; import com.crm.lead.deadline.LeadDeadlines;
import com.crm.lead.domain.dto.ConvertibleLeadView;
import com.crm.lead.domain.dto.LeadDTO; import com.crm.lead.domain.dto.LeadDTO;
import com.crm.lead.domain.dto.LeadBatchFailItem; import com.crm.lead.domain.dto.LeadBatchFailItem;
import com.crm.lead.domain.dto.LeadFeedbackDTO; import com.crm.lead.domain.dto.LeadFeedbackDTO;
@ -105,16 +104,6 @@ public class LeadServiceImpl extends BaseServiceImpl<LeadMapper, Lead>
return leadViewQuery.countStats(param); return leadViewQuery.countStats(param);
} }
@Override
public List<ConvertibleLeadView> listConvertibleLeads(Long ownerUserId) {
List<Lead> leads = getBaseMapper().selectList(new LambdaQueryWrapper<Lead>()
.eq(Lead::getOwnerUserId, ownerUserId)
.in(Lead::getStatus, LeadConstants.STATUS_CLAIMED, LeadConstants.STATUS_FOLLOWING));
return leads.stream()
.map(l -> new ConvertibleLeadView(l.getId(), l.getLeadName(), l.getPhone()))
.toList();
}
// ==================== 创建/编辑/删除 ==================== // ==================== 创建/编辑/删除 ====================
@Override @Override

33
crm-lead/src/test/java/com/crm/lead/service/impl/LeadConvertibleQueryTest.java → crm-lead/src/test/java/com/crm/lead/port/inbound/ConvertibleLeadCatalogPortImplTest.java

@ -1,43 +1,38 @@
package com.crm.lead.service.impl; package com.crm.lead.port.inbound;
import com.crm.lead.constant.LeadConstants; import com.crm.lead.constant.LeadConstants;
import com.crm.lead.domain.dto.ConvertibleLeadView;
import com.crm.lead.domain.entity.Lead; import com.crm.lead.domain.entity.Lead;
import com.crm.lead.mapper.LeadMapper; import com.crm.lead.mapper.LeadMapper;
import com.crm.lead.port.inbound.impl.ConvertibleLeadCatalogPortImpl;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* 12可转商机候选线索查询{@link LeadServiceImpl#listConvertibleLeads}规格验证 * 12 / Candidate #2可转商机候选线索目录端口
* {@link ConvertibleLeadCatalogPortImpl}逻辑自 LeadServiceImpl 原样迁入规格验证
* <p>准入 = 该用户名下 status IN (已领取 3 / 跟进中 4)投影为轻量 view</p> * <p>准入 = 该用户名下 status IN (已领取 3 / 跟进中 4)投影为轻量 view</p>
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@DisplayName("票12 可转商机候选线索查询") @DisplayName("票12 可转商机候选线索目录端口")
class LeadConvertibleQueryTest { class ConvertibleLeadCatalogPortImplTest {
private static final Long OWNER = 100L; private static final Long OWNER = 100L;
@Mock private LeadMapper leadMapper; @Mock
private LeadMapper leadMapper;
private LeadServiceImpl service() { @InjectMocks
// 仅测 listConvertibleLeads:其余构造依赖本方法不触及,传 null 即可 private ConvertibleLeadCatalogPortImpl catalog;
LeadServiceImpl svc = new LeadServiceImpl(
null, null, null, null, null, null, null, null, null, null, null);
ReflectionTestUtils.setField(svc, "baseMapper", leadMapper);
return svc;
}
private Lead lead(Long id, String name, String phone) { private Lead lead(Long id, String name, String phone) {
Lead l = new Lead(); Lead l = new Lead();
@ -54,7 +49,7 @@ class LeadConvertibleQueryTest {
lead(1L, "线索甲", "13800000001"), lead(1L, "线索甲", "13800000001"),
lead(2L, "线索乙", "13800000002"))); lead(2L, "线索乙", "13800000002")));
List<ConvertibleLeadView> views = service().listConvertibleLeads(OWNER); List<ConvertibleLeadView> views = catalog.listConvertibleLeads(OWNER);
assertThat(views).extracting(ConvertibleLeadView::id).containsExactly(1L, 2L); assertThat(views).extracting(ConvertibleLeadView::id).containsExactly(1L, 2L);
assertThat(views).extracting(ConvertibleLeadView::leadName).containsExactly("线索甲", "线索乙"); assertThat(views).extracting(ConvertibleLeadView::leadName).containsExactly("线索甲", "线索乙");
@ -62,7 +57,7 @@ class LeadConvertibleQueryTest {
} }
@Test @Test
@DisplayName("准入状态常量对齐:STATUS_CLAIMED=3 / STATUS_FOLLOWING=4") @DisplayName("准入状态常量对齐:STATUS_CLAIMED=3 / STATUS_FOLLOWING=4(与转商机前置对称)")
void statusConstantsAlignWithConvertPrecondition() { void statusConstantsAlignWithConvertPrecondition() {
assertThat(LeadConstants.STATUS_CLAIMED).isEqualTo(3); assertThat(LeadConstants.STATUS_CLAIMED).isEqualTo(3);
assertThat(LeadConstants.STATUS_FOLLOWING).isEqualTo(4); assertThat(LeadConstants.STATUS_FOLLOWING).isEqualTo(4);
@ -73,6 +68,6 @@ class LeadConvertibleQueryTest {
void listConvertibleLeads_emptyWhenNoCandidates() { void listConvertibleLeads_emptyWhenNoCandidates() {
when(leadMapper.selectList(any())).thenReturn(List.of()); when(leadMapper.selectList(any())).thenReturn(List.of());
assertThat(service().listConvertibleLeads(OWNER)).isEmpty(); assertThat(catalog.listConvertibleLeads(OWNER)).isEmpty();
} }
} }

2
crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityCreateController.java

@ -2,7 +2,7 @@ package com.crm.opportunity.controller;
import com.crm.base.domain.result.Result; import com.crm.base.domain.result.Result;
import com.crm.base.security.SecurityUtils; import com.crm.base.security.SecurityUtils;
import com.crm.lead.domain.dto.ConvertibleLeadView; import com.crm.lead.port.inbound.ConvertibleLeadView;
import com.crm.opportunity.domain.dto.CreateOpportunityRequest; import com.crm.opportunity.domain.dto.CreateOpportunityRequest;
import com.crm.opportunity.service.IOpportunityCreateService; import com.crm.opportunity.service.IOpportunityCreateService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;

12
crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityCreateService.java

@ -1,5 +1,6 @@
package com.crm.opportunity.service; package com.crm.opportunity.service;
import com.crm.lead.port.inbound.ConvertibleLeadView;
import com.crm.opportunity.domain.dto.CreateOpportunityRequest; import com.crm.opportunity.domain.dto.CreateOpportunityRequest;
import java.util.List; import java.util.List;
@ -7,9 +8,9 @@ import java.util.List;
/** /**
* 商机侧直接新建商机服务 12新建入口 A3-1-1-2-1 * 商机侧直接新建商机服务 12新建入口 A3-1-1-2-1
* *
* <p>与线索侧转商机 port 并列的第二条建商机入口核心建商机逻辑主表 + 阶段落位 + * <p>与线索侧转商机 port 并列的第二条建档入口核心建档逻辑收敛于深模块 OpportunityIntake
* 初始日志 + 团队成员 02/16两条入口对称本入口额外负责 oppSource 驱动的 * ADR-0030本入口额外负责 oppSource 驱动的关联线索必填校验候选线索准入由
* 关联线索必填校验 + source_lead_id 唯一性预检</p> * crm-lead 入站端口 {@code ConvertibleLeadCatalogPort} 提供ADR-0031本侧补占用过滤</p>
*/ */
public interface IOpportunityCreateService { public interface IOpportunityCreateService {
@ -23,9 +24,10 @@ public interface IOpportunityCreateService {
Long createOpportunity(CreateOpportunityRequest request, Long ownerUserId); Long createOpportunity(CreateOpportunityRequest request, Long ownerUserId);
/** /**
* 可关联线索下拉候选 12该用户 status IN(3,4) 且未被任何商机占用的线索 * 可关联线索下拉候选 12该用户 status IN(3,4) 且未被任何商机占用的线索
* 准入来自 {@code ConvertibleLeadCatalogPort}占用过滤在本侧
* *
* @param ownerUserId 当前登录人 * @param ownerUserId 当前登录人
*/ */
List<com.crm.lead.domain.dto.ConvertibleLeadView> listConvertibleLeads(Long ownerUserId); List<ConvertibleLeadView> listConvertibleLeads(Long ownerUserId);
} }

14
crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCreateServiceImpl.java

@ -2,8 +2,8 @@ package com.crm.opportunity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.crm.base.domain.exception.BusinessErrorException; import com.crm.base.domain.exception.BusinessErrorException;
import com.crm.lead.domain.dto.ConvertibleLeadView; import com.crm.lead.port.inbound.ConvertibleLeadCatalogPort;
import com.crm.lead.service.ILeadService; import com.crm.lead.port.inbound.ConvertibleLeadView;
import com.crm.opportunity.domain.dto.CreateOpportunityRequest; import com.crm.opportunity.domain.dto.CreateOpportunityRequest;
import com.crm.opportunity.domain.entity.Opportunity; import com.crm.opportunity.domain.entity.Opportunity;
import com.crm.opportunity.intake.IntakeSource; import com.crm.opportunity.intake.IntakeSource;
@ -34,9 +34,9 @@ import java.util.stream.Collectors;
* <p>事务本层不再标 @Transactional{@code open} 自带 REQUIRED 事务保证原子性 * <p>事务本层不再标 @Transactional{@code open} 自带 REQUIRED 事务保证原子性
* ownerDeptId null直接新建不带部门快照阶段模板走默认兜底 15与收敛前行为一致</p> * ownerDeptId null直接新建不带部门快照阶段模板走默认兜底 15与收敛前行为一致</p>
* *
* <p><b>候选线索</b> 12 架构甲准入status IN 3/4 crm-lead * <p><b>候选线索</b> 12 架构甲准入status IN 3/4 crm-lead 入站端口
* {@link ILeadService#listConvertibleLeads} 在线索域判定未被商机占用由本侧补 * {@link ConvertibleLeadCatalogPort} 在线索域判定ADR-0031不再 import 宽服务接口
* opportunity.source_lead_id职责边界干净此能力属于 Candidate #2 范围维持原状</p> * 未被商机占用由本侧补 opportunity.source_lead_id职责边界干净</p>
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -44,7 +44,7 @@ public class OpportunityCreateServiceImpl implements IOpportunityCreateService {
private final OpportunityIntake intake; private final OpportunityIntake intake;
private final OpportunityMapper oppMapper; private final OpportunityMapper oppMapper;
private final ILeadService leadService; private final ConvertibleLeadCatalogPort convertibleLeadCatalog;
@Override @Override
public Long createOpportunity(CreateOpportunityRequest request, Long ownerUserId) { public Long createOpportunity(CreateOpportunityRequest request, Long ownerUserId) {
@ -80,7 +80,7 @@ public class OpportunityCreateServiceImpl implements IOpportunityCreateService {
@Override @Override
public List<ConvertibleLeadView> listConvertibleLeads(Long ownerUserId) { public List<ConvertibleLeadView> listConvertibleLeads(Long ownerUserId) {
List<ConvertibleLeadView> candidates = leadService.listConvertibleLeads(ownerUserId); List<ConvertibleLeadView> candidates = convertibleLeadCatalog.listConvertibleLeads(ownerUserId);
if (candidates.isEmpty()) { if (candidates.isEmpty()) {
return candidates; return candidates;
} }

11
crm-opportunity/src/test/java/com/crm/opportunity/service/impl/OpportunityCreateServiceImplTest.java

@ -3,8 +3,8 @@ package com.crm.opportunity.service.impl;
import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.crm.base.domain.exception.BusinessErrorException; import com.crm.base.domain.exception.BusinessErrorException;
import com.crm.lead.domain.dto.ConvertibleLeadView; import com.crm.lead.port.inbound.ConvertibleLeadCatalogPort;
import com.crm.lead.service.ILeadService; import com.crm.lead.port.inbound.ConvertibleLeadView;
import com.crm.opportunity.domain.dto.CreateOpportunityRequest; import com.crm.opportunity.domain.dto.CreateOpportunityRequest;
import com.crm.opportunity.domain.entity.Opportunity; import com.crm.opportunity.domain.entity.Opportunity;
import com.crm.opportunity.intake.IntakeSource; import com.crm.opportunity.intake.IntakeSource;
@ -37,7 +37,8 @@ import static org.mockito.Mockito.when;
* <p>覆盖oppSource=线索转入时 sourceLeadId 必填校验不进深模块 * <p>覆盖oppSource=线索转入时 sourceLeadId 必填校验不进深模块
* {@code CreateOpportunityRequest} {@code OpportunityIntakeSpec} 全字段映射 * {@code CreateOpportunityRequest} {@code OpportunityIntakeSpec} 全字段映射
* {@code OpportunityIntakeException} {@code BusinessErrorException} seam 翻译 * {@code OpportunityIntakeException} {@code BusinessErrorException} seam 翻译
* 候选线索剔除已占用建档五步机制与唯一性防护的断言已上移 * 候选线索剔除已占用准入来自 {@code ConvertibleLeadCatalogPort}ADR-0031
* 建档五步机制与唯一性防护的断言已上移
* {@code OpportunityIntakeImplTest}本类不再重复</p> * {@code OpportunityIntakeImplTest}本类不再重复</p>
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -51,7 +52,7 @@ class OpportunityCreateServiceImplTest {
@Mock private OpportunityIntake intake; @Mock private OpportunityIntake intake;
@Mock private OpportunityMapper oppMapper; @Mock private OpportunityMapper oppMapper;
@Mock private ILeadService leadService; @Mock private ConvertibleLeadCatalogPort convertibleLeadCatalog;
@InjectMocks private OpportunityCreateServiceImpl service; @InjectMocks private OpportunityCreateServiceImpl service;
@ -131,7 +132,7 @@ class OpportunityCreateServiceImplTest {
@Test @Test
@DisplayName("候选线索:剔除已被商机占用的 source_lead_id") @DisplayName("候选线索:剔除已被商机占用的 source_lead_id")
void listConvertibleLeads_excludesTaken() { void listConvertibleLeads_excludesTaken() {
when(leadService.listConvertibleLeads(OWNER)).thenReturn(List.of( when(convertibleLeadCatalog.listConvertibleLeads(OWNER)).thenReturn(List.of(
new ConvertibleLeadView(1L, "线索甲", "138"), new ConvertibleLeadView(1L, "线索甲", "138"),
new ConvertibleLeadView(2L, "线索乙", "139"))); new ConvertibleLeadView(2L, "线索乙", "139")));
// 商机侧查已占用:lead 2 已被占用 // 商机侧查已占用:lead 2 已被占用

Loading…
Cancel
Save