Browse Source

feat(opportunity): 票10 读侧回显当前/下一节点名 (依 crm-rule 阶段模板)

- 注入 IOpportunityStageTemplateService
- fillStageNodeNames: current_stage_id → getNodeById 取当前节点
- 下一节点 = 同版本 (node.templateId) 按 seqNo 升序的下一节点, 末节点→空
- 节点展示名 = customNodeName 优先, 空则带入 opp_stage 字典名 (票06口径)
- 批量: distinct stageId/templateId/dictCode 去重取值, 无 N+1
- 新增当前+下一节点名 / 末节点无下一 两测试, 132 全绿
master
luoweijian 2 weeks ago
parent
commit
a7af0ac8ca
  1. 95
      crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunityViewQueryImpl.java
  2. 78
      crm-opportunity/src/test/java/com/crm/opportunity/query/impl/OpportunityViewQueryImplTest.java

95
crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunityViewQueryImpl.java

@ -31,6 +31,8 @@ import com.crm.opportunity.mapper.OpportunityStageHistoryMapper;
import com.crm.opportunity.mapper.OpportunityViewLogMapper; import com.crm.opportunity.mapper.OpportunityViewLogMapper;
import com.crm.opportunity.query.OpportunityViewQuery; import com.crm.opportunity.query.OpportunityViewQuery;
import com.crm.rule.domain.dto.SysRegionDTO; import com.crm.rule.domain.dto.SysRegionDTO;
import com.crm.rule.domain.entity.OpportunityStageNode;
import com.crm.rule.service.IOpportunityStageTemplateService;
import com.crm.rule.service.ISysRegionService; import com.crm.rule.service.ISysRegionService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -55,11 +57,10 @@ import java.util.stream.Collectors;
* *
* <p> 10 §二数值计算列节点停留/最近跟进/方案卡状态/方案预算已批量 join 填充 N+1 * <p> 10 §二数值计算列节点停留/最近跟进/方案卡状态/方案预算已批量 join 填充 N+1
* 字典/部门/区划名回显DictQueryService/SysDeptService/SysRegionService对称 crm-lead fillDisplayFields * 字典/部门/区划名回显DictQueryService/SysDeptService/SysRegionService对称 crm-lead fillDisplayFields
* 已落</p> * 及当前/下一节点名IOpportunityStageTemplateService已落</p>
* *
* <p><b>TODO后续步骤</b> 当前/下一节点名依赖 crm-rule 阶段模板服务 PARTICIPATED * <p><b>TODO后续步骤</b>PARTICIPATED 团队白名单可见性叠加详情链路RECENT 视图已按
* 团队白名单可见性叠加详情链路RECENT 视图已按 {@code opportunity_view_log.last_view_time} 倒序取上限 * {@code opportunity_view_log.last_view_time} 倒序取上限 {@link #pageRecent}</p>
* {@link #pageRecent}</p>
*/ */
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@ -75,6 +76,7 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery {
private final DictQueryService dictQueryService; private final DictQueryService dictQueryService;
private final ISysDeptService sysDeptService; private final ISysDeptService sysDeptService;
private final ISysRegionService sysRegionService; private final ISysRegionService sysRegionService;
private final IOpportunityStageTemplateService stageTemplateService;
/** RECENT 视图上限(票 10 P2-2 默认 50;view_log 已被后台 Job 裁剪,此处再兜底截断)。 */ /** RECENT 视图上限(票 10 P2-2 默认 50;view_log 已被后台 Job 裁剪,此处再兜底截断)。 */
private static final int RECENT_LIMIT = 50; private static final int RECENT_LIMIT = 50;
@ -208,6 +210,7 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery {
fillDictNames(dtos); fillDictNames(dtos);
fillDeptNames(dtos); fillDeptNames(dtos);
fillRegionNames(dtos); fillRegionNames(dtos);
fillStageNodeNames(dtos);
} }
/** /**
@ -269,6 +272,90 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery {
}); });
} }
/**
* 当前/下一节点名回显 10 §二口径 crm-rule
* {@link IOpportunityStageTemplateService}
* <ul>
* <li>当前节点 = {@code current_stage_id}本身即节点 id对应节点
* 展示名 = {@code customNodeName} 非空则取之否则带入 {@code opp_stage}
* 字典中 {@code stageDictCode} 的名 06 空则带入字典名</li>
* <li>下一节点 = 当前节点所属模板版本{@code node.templateId}下按 {@code seqNo}
* 升序的下一节点已处末节点如已转项目 </li>
* </ul>
* <p>批量 distinct {@code currentStageId} 取节点同页去重 distinct
* {@code templateId} 取版本节点列表stageDictCode 字典名也去重后查避免 N+1</p>
*/
private void fillStageNodeNames(List<OpportunityDTO> dtos) {
Set<Long> stageIds = dtos.stream().map(OpportunityDTO::getCurrentStageId)
.filter(java.util.Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
if (stageIds.isEmpty()) {
return;
}
// 1. 当前节点:distinct id → node
Map<Long, OpportunityStageNode> currentNodes = new HashMap<>();
for (Long stageId : stageIds) {
OpportunityStageNode node = stageTemplateService.getNodeById(stageId);
if (node != null) {
currentNodes.put(stageId, node);
}
}
// 2. 下一节点:按 distinct templateId 取整版节点列表,缓存。
Map<Long, List<OpportunityStageNode>> versionNodes = new HashMap<>();
for (OpportunityStageNode node : currentNodes.values()) {
versionNodes.computeIfAbsent(node.getTemplateId(),
tplId -> stageTemplateService.listNodesOfVersion(tplId));
}
// 3. 字典名带入(distinct stageDictCode)
Set<String> dictCodes = new HashSet<>();
for (OpportunityStageNode node : currentNodes.values()) {
collectDictCode(dictCodes, node);
List<OpportunityStageNode> siblings = versionNodes.get(node.getTemplateId());
OpportunityStageNode next = nextNode(siblings, node);
if (next != null) {
collectDictCode(dictCodes, next);
}
}
Map<String, String> stageDictNames = dictNames(
com.crm.rule.constant.OpportunityRuleConstants.STAGE_DICT_GROUP_CODE, dictCodes);
dtos.forEach(d -> {
OpportunityStageNode node = currentNodes.get(d.getCurrentStageId());
if (node == null) {
return;
}
d.setCurrentStageName(nodeDisplayName(node, stageDictNames));
OpportunityStageNode next = nextNode(versionNodes.get(node.getTemplateId()), node);
if (next != null) {
d.setNextStageName(nodeDisplayName(next, stageDictNames));
}
});
}
private void collectDictCode(Set<String> codes, OpportunityStageNode node) {
if (StrUtil.isBlank(node.getCustomNodeName()) && StrUtil.isNotBlank(node.getStageDictCode())) {
codes.add(node.getStageDictCode());
}
}
/** 节点展示名:customNodeName 优先,空则带入 opp_stage 字典名(票 06)。 */
private String nodeDisplayName(OpportunityStageNode node, Map<String, String> stageDictNames) {
if (StrUtil.isNotBlank(node.getCustomNodeName())) {
return node.getCustomNodeName();
}
return stageDictNames.get(node.getStageDictCode());
}
/** 同版本内按 seqNo 升序取 current 的下一节点;无则 null。 */
private OpportunityStageNode nextNode(List<OpportunityStageNode> siblings, OpportunityStageNode current) {
if (CollUtil.isEmpty(siblings) || current.getSeqNo() == null) {
return null;
}
return siblings.stream()
.filter(n -> n.getSeqNo() != null && n.getSeqNo() > current.getSeqNo())
.min(java.util.Comparator.comparingInt(OpportunityStageNode::getSeqNo))
.orElse(null);
}
/** /**
* 节点停留时间= 进入当前节点日期 * 节点停留时间= 进入当前节点日期
* <p>进入日期 = {@code opportunity_stage_history} {@code to_stage_id = current_stage_id} 的最新 * <p>进入日期 = {@code opportunity_stage_history} {@code to_stage_id = current_stage_id} 的最新

78
crm-opportunity/src/test/java/com/crm/opportunity/query/impl/OpportunityViewQueryImplTest.java

@ -70,6 +70,8 @@ class OpportunityViewQueryImplTest {
private com.crm.auth.service.ISysDeptService sysDeptService; private com.crm.auth.service.ISysDeptService sysDeptService;
@Mock @Mock
private com.crm.rule.service.ISysRegionService sysRegionService; private com.crm.rule.service.ISysRegionService sysRegionService;
@Mock
private com.crm.rule.service.IOpportunityStageTemplateService stageTemplateService;
@InjectMocks @InjectMocks
private OpportunityViewQueryImpl query; private OpportunityViewQueryImpl query;
@ -88,6 +90,8 @@ class OpportunityViewQueryImplTest {
lenient().when(viewLogMapper.selectList(any())).thenReturn(Collections.emptyList()); lenient().when(viewLogMapper.selectList(any())).thenReturn(Collections.emptyList());
lenient().when(sysDeptService.listByIds(any())).thenReturn(Collections.emptyList()); lenient().when(sysDeptService.listByIds(any())).thenReturn(Collections.emptyList());
lenient().when(sysRegionService.listByCodes(any())).thenReturn(Collections.emptyList()); lenient().when(sysRegionService.listByCodes(any())).thenReturn(Collections.emptyList());
lenient().when(stageTemplateService.getNodeById(any())).thenReturn(null);
lenient().when(stageTemplateService.listNodesOfVersion(any())).thenReturn(Collections.emptyList());
} }
@AfterEach @AfterEach
@ -287,4 +291,78 @@ class OpportunityViewQueryImplTest {
assertThat(dto.getProvinceName()).isEqualTo("广东省"); assertThat(dto.getProvinceName()).isEqualTo("广东省");
assertThat(dto.getCityName()).isEqualTo("深圳市"); assertThat(dto.getCityName()).isEqualTo("深圳市");
} }
@Test
@DisplayName("节点名:当前节点名(customNodeName 优先/字典名兵底)+ 下一节点(seq 升序)")
@SuppressWarnings("unchecked")
void stageNodeNames_currentAndNext() {
// opp 当前在节点 200(模板版本 8001)
Opportunity opp = new Opportunity();
opp.setId(7001L);
opp.setCurrentStageId(200L);
Page<Opportunity> page = new Page<>();
page.setRecords(List.of(opp));
when(opportunityMapper.selectPage(any(), any())).thenReturn(page);
// 当前节点 200:seq=2, 无 customNodeName → 带入 opp_stage 字典名
com.crm.rule.domain.entity.OpportunityStageNode cur = new com.crm.rule.domain.entity.OpportunityStageNode();
cur.setId(200L);
cur.setTemplateId(8001L);
cur.setSeqNo(2);
cur.setStageDictCode("OPP_STAGE_02");
when(stageTemplateService.getNodeById(200L)).thenReturn(cur);
// 版本 8001 节点列:seq1 / seq2(当前) / seq3(下一,有 customNodeName)
com.crm.rule.domain.entity.OpportunityStageNode n1 = new com.crm.rule.domain.entity.OpportunityStageNode();
n1.setId(100L);
n1.setTemplateId(8001L);
n1.setSeqNo(1);
n1.setStageDictCode("OPP_STAGE_01");
com.crm.rule.domain.entity.OpportunityStageNode n3 = new com.crm.rule.domain.entity.OpportunityStageNode();
n3.setId(300L);
n3.setTemplateId(8001L);
n3.setSeqNo(3);
n3.setStageDictCode("OPP_STAGE_03");
n3.setCustomNodeName("自定义节点名");
when(stageTemplateService.listNodesOfVersion(8001L)).thenReturn(List.of(cur, n1, n3));
// 字典:OPP_STAGE_02 → 关系摸排(当前节点带入)
com.crm.dict.domain.entity.DictItem stageItem = new com.crm.dict.domain.entity.DictItem();
stageItem.setName("关系摸排");
when(dictQueryService.getItem("opp_stage", "OPP_STAGE_02")).thenReturn(stageItem);
OpportunityPageParam param = new OpportunityPageParam();
param.setViewType("MINE");
OpportunityDTO dto = query.pageOpportunities(param).getContent().get(0);
assertThat(dto.getCurrentStageName()).isEqualTo("关系摸排");
assertThat(dto.getNextStageName()).isEqualTo("自定义节点名");
}
@Test
@DisplayName("节点名:已处末节点 → 下一节点名为空")
@SuppressWarnings("unchecked")
void stageNodeNames_lastNode_noNext() {
Opportunity opp = new Opportunity();
opp.setId(7001L);
opp.setCurrentStageId(500L);
Page<Opportunity> page = new Page<>();
page.setRecords(List.of(opp));
when(opportunityMapper.selectPage(any(), any())).thenReturn(page);
com.crm.rule.domain.entity.OpportunityStageNode last = new com.crm.rule.domain.entity.OpportunityStageNode();
last.setId(500L);
last.setTemplateId(8001L);
last.setSeqNo(5);
last.setCustomNodeName("已转项目");
when(stageTemplateService.getNodeById(500L)).thenReturn(last);
when(stageTemplateService.listNodesOfVersion(8001L)).thenReturn(List.of(last));
OpportunityPageParam param = new OpportunityPageParam();
param.setViewType("MINE");
OpportunityDTO dto = query.pageOpportunities(param).getContent().get(0);
assertThat(dto.getCurrentStageName()).isEqualTo("已转项目");
assertThat(dto.getNextStageName()).isNull();
}
} }

Loading…
Cancel
Save