|
|
@ -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} 的最新 |
|
|
|