|
|
@ -9,17 +9,30 @@ import com.crm.base.security.SecurityUtils; |
|
|
import com.crm.base.utils.PageConverter; |
|
|
import com.crm.base.utils.PageConverter; |
|
|
import com.crm.opportunity.domain.dto.OpportunityDTO; |
|
|
import com.crm.opportunity.domain.dto.OpportunityDTO; |
|
|
import com.crm.opportunity.domain.entity.Opportunity; |
|
|
import com.crm.opportunity.domain.entity.Opportunity; |
|
|
|
|
|
import com.crm.opportunity.domain.entity.OpportunityCustomer; |
|
|
import com.crm.opportunity.domain.entity.OpportunityFocus; |
|
|
import com.crm.opportunity.domain.entity.OpportunityFocus; |
|
|
|
|
|
import com.crm.opportunity.domain.entity.OpportunityFollow; |
|
|
|
|
|
import com.crm.opportunity.domain.entity.OpportunitySchemeCard; |
|
|
|
|
|
import com.crm.opportunity.domain.entity.OpportunityStageHistory; |
|
|
import com.crm.opportunity.domain.enums.OpportunityStatus; |
|
|
import com.crm.opportunity.domain.enums.OpportunityStatus; |
|
|
import com.crm.opportunity.domain.enums.OpportunityViewType; |
|
|
import com.crm.opportunity.domain.enums.OpportunityViewType; |
|
|
import com.crm.opportunity.domain.param.OpportunityPageParam; |
|
|
import com.crm.opportunity.domain.param.OpportunityPageParam; |
|
|
|
|
|
import com.crm.opportunity.mapper.OpportunityCustomerMapper; |
|
|
import com.crm.opportunity.mapper.OpportunityFocusMapper; |
|
|
import com.crm.opportunity.mapper.OpportunityFocusMapper; |
|
|
|
|
|
import com.crm.opportunity.mapper.OpportunityFollowMapper; |
|
|
import com.crm.opportunity.mapper.OpportunityMapper; |
|
|
import com.crm.opportunity.mapper.OpportunityMapper; |
|
|
|
|
|
import com.crm.opportunity.mapper.OpportunitySchemeCardMapper; |
|
|
|
|
|
import com.crm.opportunity.mapper.OpportunityStageHistoryMapper; |
|
|
import com.crm.opportunity.query.OpportunityViewQuery; |
|
|
import com.crm.opportunity.query.OpportunityViewQuery; |
|
|
import lombok.RequiredArgsConstructor; |
|
|
import lombok.RequiredArgsConstructor; |
|
|
import org.springframework.stereotype.Component; |
|
|
import org.springframework.stereotype.Component; |
|
|
|
|
|
|
|
|
|
|
|
import java.time.LocalDate; |
|
|
|
|
|
import java.time.LocalDateTime; |
|
|
|
|
|
import java.time.temporal.ChronoUnit; |
|
|
|
|
|
import java.util.HashMap; |
|
|
import java.util.List; |
|
|
import java.util.List; |
|
|
|
|
|
import java.util.Map; |
|
|
import java.util.Set; |
|
|
import java.util.Set; |
|
|
import java.util.stream.Collectors; |
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
|
@ -30,10 +43,12 @@ import java.util.stream.Collectors; |
|
|
* {@link #applyViewFilters};{@code @DataScope}({@code Opportunity} 实体注解)经拦截器对 wrapper 自动 |
|
|
* {@link #applyViewFilters};{@code @DataScope}({@code Opportunity} 实体注解)经拦截器对 wrapper 自动 |
|
|
* 追加部门天花板。当前步已落:视图 filter + 公共筛选 + 分页 + followed 回显。</p> |
|
|
* 追加部门天花板。当前步已落:视图 filter + 公共筛选 + 分页 + followed 回显。</p> |
|
|
* |
|
|
* |
|
|
* <p><b>TODO(后续步骤)</b>:① 字典/部门/区划名回显(DictQueryService/SysDeptService/SysRegionService, |
|
|
* <p>票 10 §二数值计算列(节点停留/最近跟进/方案卡状态/方案预算)已批量 join 填充(无 N+1)。</p> |
|
|
* 对称 crm-lead fillDisplayFields);② 票 10 §二计算列 join(节点停留/最近跟进/方案卡状态/方案预算/入池前节点); |
|
|
* |
|
|
* ③ RECENT 视图按 {@code opportunity_view_log.last_view_time} 倒序取上限(当前 RECENT 先按 create_time |
|
|
* <p><b>TODO(后续步骤)</b>:① 字典/部门/区划名回显(DictQueryService/SysDeptService/SysRegionService, |
|
|
* 兜底排序,待接 view_log join);④ PARTICIPATED 团队白名单可见性叠加详情链路。</p> |
|
|
* 对称 crm-lead fillDisplayFields);② 当前/下一节点名(依赖 crm-rule 阶段模板服务);③ RECENT 视图按 |
|
|
|
|
|
* {@code opportunity_view_log.last_view_time} 倒序取上限(当前 RECENT 先按 create_time 兜底排序,待接 |
|
|
|
|
|
* view_log join);④ PARTICIPATED 团队白名单可见性叠加详情链路。</p> |
|
|
*/ |
|
|
*/ |
|
|
@Component |
|
|
@Component |
|
|
@RequiredArgsConstructor |
|
|
@RequiredArgsConstructor |
|
|
@ -41,6 +56,10 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery { |
|
|
|
|
|
|
|
|
private final OpportunityMapper opportunityMapper; |
|
|
private final OpportunityMapper opportunityMapper; |
|
|
private final OpportunityFocusMapper opportunityFocusMapper; |
|
|
private final OpportunityFocusMapper opportunityFocusMapper; |
|
|
|
|
|
private final OpportunityStageHistoryMapper stageHistoryMapper; |
|
|
|
|
|
private final OpportunityFollowMapper followMapper; |
|
|
|
|
|
private final OpportunityCustomerMapper customerMapper; |
|
|
|
|
|
private final OpportunitySchemeCardMapper schemeCardMapper; |
|
|
|
|
|
|
|
|
@Override |
|
|
@Override |
|
|
public PageResult<OpportunityDTO> pageOpportunities(OpportunityPageParam param) { |
|
|
public PageResult<OpportunityDTO> pageOpportunities(OpportunityPageParam param) { |
|
|
@ -107,8 +126,8 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 出参回显字段填充。当前步:followed(当前用户是否关注)。 |
|
|
* 出参回显字段填充:followed + 票 10 §二数值计算列(批量,无 N+1)。 |
|
|
* <p>TODO 后续:字典/部门/区划名 + 票 10 §二计算列(节点停留/最近跟进/方案卡状态/方案预算/入池前节点)。</p> |
|
|
* <p>TODO 后续:字典/部门/区划名 + 当前/下一节点名(依赖 crm-rule 模板服务)。</p> |
|
|
*/ |
|
|
*/ |
|
|
private void fillDisplayFields(List<OpportunityDTO> dtos, Long currentUserId) { |
|
|
private void fillDisplayFields(List<OpportunityDTO> dtos, Long currentUserId) { |
|
|
if (CollUtil.isEmpty(dtos)) { |
|
|
if (CollUtil.isEmpty(dtos)) { |
|
|
@ -123,6 +142,91 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery { |
|
|
.eq(OpportunityFocus::getUserId, currentUserId)) |
|
|
.eq(OpportunityFocus::getUserId, currentUserId)) |
|
|
.stream().map(OpportunityFocus::getOpportunityId).collect(Collectors.toSet()); |
|
|
.stream().map(OpportunityFocus::getOpportunityId).collect(Collectors.toSet()); |
|
|
dtos.forEach(d -> d.setFollowed(followedIds.contains(d.getId()))); |
|
|
dtos.forEach(d -> d.setFollowed(followedIds.contains(d.getId()))); |
|
|
|
|
|
|
|
|
|
|
|
fillStageStayDays(dtos, ids); |
|
|
|
|
|
fillLastFollowTime(dtos, ids); |
|
|
|
|
|
fillSchemeCardColumns(dtos, ids); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 节点停留时间(天)= 今 − 进入当前节点日期。 |
|
|
|
|
|
* <p>进入日期 = {@code opportunity_stage_history} 中 {@code to_stage_id = current_stage_id} 的最新 |
|
|
|
|
|
* {@code operate_time}(最近一次迁入当前节点)。无迁移记录(如新建即在初始节点未记史)→ 空。</p> |
|
|
|
|
|
*/ |
|
|
|
|
|
private void fillStageStayDays(List<OpportunityDTO> dtos, List<Long> ids) { |
|
|
|
|
|
List<OpportunityStageHistory> histories = stageHistoryMapper.selectList( |
|
|
|
|
|
new LambdaQueryWrapper<OpportunityStageHistory>() |
|
|
|
|
|
.in(OpportunityStageHistory::getOppId, ids)); |
|
|
|
|
|
// (oppId, toStageId) -> 最新 operateTime
|
|
|
|
|
|
Map<Long, Map<Long, LocalDateTime>> byOpp = new HashMap<>(); |
|
|
|
|
|
for (OpportunityStageHistory h : histories) { |
|
|
|
|
|
if (h.getToStageId() == null || h.getOperateTime() == null) { |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
byOpp.computeIfAbsent(h.getOppId(), k -> new HashMap<>()) |
|
|
|
|
|
.merge(h.getToStageId(), h.getOperateTime(), |
|
|
|
|
|
(a, b) -> a.isAfter(b) ? a : b); |
|
|
|
|
|
} |
|
|
|
|
|
LocalDate today = LocalDate.now(); |
|
|
|
|
|
dtos.forEach(d -> { |
|
|
|
|
|
if (d.getCurrentStageId() == null) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
Map<Long, LocalDateTime> stageTimes = byOpp.get(d.getId()); |
|
|
|
|
|
if (stageTimes == null) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
LocalDateTime enterTime = stageTimes.get(d.getCurrentStageId()); |
|
|
|
|
|
if (enterTime != null) { |
|
|
|
|
|
d.setStageStayDays(ChronoUnit.DAYS.between(enterTime.toLocalDate(), today)); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** 最近跟进 = 最新一条 {@code opportunity_follow.follow_time}(票 09);无则空。 */ |
|
|
|
|
|
private void fillLastFollowTime(List<OpportunityDTO> dtos, List<Long> ids) { |
|
|
|
|
|
List<OpportunityFollow> follows = followMapper.selectList( |
|
|
|
|
|
new LambdaQueryWrapper<OpportunityFollow>() |
|
|
|
|
|
.in(OpportunityFollow::getOppId, ids) |
|
|
|
|
|
.isNotNull(OpportunityFollow::getFollowTime)); |
|
|
|
|
|
Map<Long, LocalDateTime> latest = follows.stream().collect(Collectors.toMap( |
|
|
|
|
|
OpportunityFollow::getOppId, OpportunityFollow::getFollowTime, |
|
|
|
|
|
(a, b) -> a.isAfter(b) ? a : b)); |
|
|
|
|
|
dtos.forEach(d -> d.setLastFollowTime(latest.get(d.getId()))); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 方案卡状态 + 方案预算 = 取主要意向客户({@code opportunity_customer.is_primary_intended=1})那张 |
|
|
|
|
|
* 方案卡的 {@code card_status} / {@code scheme_budget}(票 07/13,一客户一卡故取值唯一)。 |
|
|
|
|
|
* <p>无主客户或主客户无卡 → 两列均空(前端渲染「未创建」)。</p> |
|
|
|
|
|
*/ |
|
|
|
|
|
private void fillSchemeCardColumns(List<OpportunityDTO> dtos, List<Long> ids) { |
|
|
|
|
|
// 1. 每个 opp 的主要意向客户 customerId
|
|
|
|
|
|
Map<Long, Long> primaryCustomer = customerMapper.selectList( |
|
|
|
|
|
new LambdaQueryWrapper<OpportunityCustomer>() |
|
|
|
|
|
.in(OpportunityCustomer::getOpportunityId, ids) |
|
|
|
|
|
.eq(OpportunityCustomer::getIsPrimaryIntended, 1)) |
|
|
|
|
|
.stream().collect(Collectors.toMap( |
|
|
|
|
|
OpportunityCustomer::getOpportunityId, |
|
|
|
|
|
OpportunityCustomer::getCustomerId, (a, b) -> a)); |
|
|
|
|
|
if (primaryCustomer.isEmpty()) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
// 2. 主客户那张方案卡(按 oppId + customerId 命中)
|
|
|
|
|
|
List<OpportunitySchemeCard> cards = schemeCardMapper.selectList( |
|
|
|
|
|
new LambdaQueryWrapper<OpportunitySchemeCard>() |
|
|
|
|
|
.in(OpportunitySchemeCard::getOppId, primaryCustomer.keySet())); |
|
|
|
|
|
Map<Long, OpportunitySchemeCard> cardByOpp = cards.stream() |
|
|
|
|
|
.filter(c -> c.getCustomerId() != null |
|
|
|
|
|
&& c.getCustomerId().equals(primaryCustomer.get(c.getOppId()))) |
|
|
|
|
|
.collect(Collectors.toMap(OpportunitySchemeCard::getOppId, c -> c, (a, b) -> a)); |
|
|
|
|
|
dtos.forEach(d -> { |
|
|
|
|
|
OpportunitySchemeCard card = cardByOpp.get(d.getId()); |
|
|
|
|
|
if (card != null) { |
|
|
|
|
|
d.setSchemeCardStatus(card.getCardStatus()); |
|
|
|
|
|
d.setSchemeBudget(card.getSchemeBudget()); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private Long getCurrentUserId() { |
|
|
private Long getCurrentUserId() { |
|
|
|