Browse Source

feat(opportunity): 票 10 读侧骨架(ViewType 枚举 + PageParam + DTO)

四视图查询层骨架(对称 crm-lead LeadViewQuery,一步到位用票 14 最终视图族):
- OpportunityViewType 六视图:RECENT/MINE/PARTICIPATED/FOLLOWED/MANAGE/
  PUBLIC_POOL,fromValue 空/非法回落 MINE(票 14 默认视图口径)
- OpportunityPageParam:viewType + 可筛选列字段池(票 14 确认点 3)
- OpportunityDTO:主表字段 + 出参回显(字典/部门/区划名)+ 票 10 §二计算列
  占位(节点停留/最近跟进/方案卡状态/方案预算/是否关注/入池前节点)
计算列聚合 + ViewQuery filter + Controller 待续。crm-opportunity 117→121 全绿。
master
luoweijian 2 weeks ago
parent
commit
29baf93a70
  1. 159
      crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityDTO.java
  2. 46
      crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/OpportunityViewType.java
  3. 59
      crm-opportunity/src/main/java/com/crm/opportunity/domain/param/OpportunityPageParam.java
  4. 73
      crm-opportunity/src/test/java/com/crm/opportunity/query/OpportunityViewQuerySkeletonTest.java

159
crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/OpportunityDTO.java

@ -0,0 +1,159 @@
package com.crm.opportunity.domain.dto;
import com.crm.base.domain.dto.BaseDTO;
import com.crm.opportunity.domain.entity.Opportunity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 商机传输对象写入参 + 出参双向ADR-0017 Route A
*
* <p>基础字段 = 商机主表{@link Opportunity} §2出参回显字段 = 10 §二列表列计算列不冗余存
* 实时算/join+ 字典/部门/人名快照名{@code fromEntity} 只拷主表字段回显字段由 ViewQuery
* fillDisplayFields 二次填充</p>
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OpportunityDTO extends BaseDTO {
// -------- 业务字段 --------
@Schema(description = "商机名称")
private String oppName;
@Schema(description = "商机来源字典 code")
private String oppSource;
@Schema(description = "商机类型字典 code")
private String oppType;
@Schema(description = "行业字典 code")
private String industryCode;
@Schema(description = "招标形式字典 code")
private String bidForm;
@Schema(description = "项目属地字典 code")
private String localityType;
@Schema(description = "甲方是否明确 0/1")
private Integer partyAClear;
@Schema(description = "甲方名称")
private String partyA;
@Schema(description = "省份国标 code")
private String provinceCode;
@Schema(description = "市/区国标 code")
private String cityCode;
@Schema(description = "备注")
private String remark;
@Schema(description = "项目金额(列表展示为「预计金额」)")
private BigDecimal projectAmount;
// -------- 归属 --------
@Schema(description = "负责人ID")
private Long ownerUserId;
@Schema(description = "负责人姓名快照")
private String ownerNameSnapshot;
@Schema(description = "负责人部门ID快照")
private Long ownerDeptId;
@Schema(description = "进入公海原因字典 code")
private String poolReason;
// -------- 两条正交轴 --------
@Schema(description = "商机状态(轴1:1待领取/2推进中/3暂缓中/4已关闭/5已转项目)")
private Integer oppStatus;
@Schema(description = "当前阶段节点ID(轴2)")
private Long currentStageId;
@Schema(description = "阶段模板版本号冗余")
private String stageTemplateVersion;
// -------- 来源线索快照 --------
@Schema(description = "来源线索ID")
private Long sourceLeadId;
// -------- 主要意向客户冗余 --------
@Schema(description = "主要意向客户ID")
private Long primaryCustomerId;
@Schema(description = "主客户名快照(列表「意向客户」列)")
private String primaryCustomerNameSnapshot;
@Schema(description = "领取时刻")
private LocalDateTime claimTime;
@Schema(description = "最近有效跟进时间")
private LocalDateTime lastValidFollowTime;
// -------- 出参回显:字典/部门/区划名 --------
@Schema(description = "商机来源名称(出参回显,字典快照)")
private String oppSourceName;
@Schema(description = "行业名称(出参回显,字典快照)")
private String industryName;
@Schema(description = "省份名称(出参回显)")
private String provinceName;
@Schema(description = "市/区名称(出参回显)")
private String cityName;
@Schema(description = "负责人部门名称(出参回显)")
private String ownerDeptName;
// -------- 出参回显:票 10 §二 计算列(实时算/join,不冗余存) --------
@Schema(description = "当前工作节点名称(出参回显,= current_stage_id 节点名,票 06)")
private String currentStageName;
@Schema(description = "节点停留时间(天,出参回显,= 今 − 进当前节点日期,opportunity_stage_history 最新 to_time)")
private Long stageStayDays;
@Schema(description = "下一节点名称(出参回显,= 模板中当前节点下一 seq_no 节点,展示用建议下一步)")
private String nextStageName;
@Schema(description = "最近跟进时间(出参回显,= 最新 opportunity_follow.create_time,票 09;无则空)")
private LocalDateTime lastFollowTime;
@Schema(description = "方案卡状态(出参回显,= 主要意向客户那张卡 card_status,票 07/13;无主客户卡=未创建0)")
private Integer schemeCardStatus;
@Schema(description = "方案预算(出参回显,= 主要意向客户那张卡 scheme_budget,票 07;无则空)")
private BigDecimal schemeBudget;
@Schema(description = "当前用户是否已关注(出参回显,opportunity_focus)")
private Boolean followed;
@Schema(description = "入池前工作节点名称(出参回显,公海视图专属,票 04/05)")
private String poolStageNameSnapshot;
public static OpportunityDTO fromEntity(Opportunity entity) {
OpportunityDTO dto = new OpportunityDTO();
org.springframework.beans.BeanUtils.copyProperties(entity, dto);
return dto;
}
public Opportunity toEntity() {
Opportunity entity = new Opportunity();
org.springframework.beans.BeanUtils.copyProperties(this, entity);
return entity;
}
}

46
crm-opportunity/src/main/java/com/crm/opportunity/domain/enums/OpportunityViewType.java

@ -0,0 +1,46 @@
package com.crm.opportunity.domain.enums;
import cn.hutool.core.util.StrUtil;
/**
* 商机列表视图类型{@link com.crm.opportunity.domain.param.OpportunityPageParam#getViewType()} 取值域
*
* <p> 14 定稿的最终视图族推翻票 10单一我负责的一步到位免返工销售机会工作区内置四视图
* 常用检索 Tab顺序 = 最近访问/我负责的/我参与的/我关注的+ 管理/公海两个独立 workspace</p>
* <ul>
* <li>{@link #RECENT} 最近访问 {@code opportunity_view_log} 命中当前用户last_view_time 倒序取上限 10 P2-2 默认 50</li>
* <li>{@link #MINE} 我负责的 {@code owner_user_id = 当前用户} 10 个人口径</li>
* <li>{@link #PARTICIPATED} 我参与的 团队成员且非负责人{@code EXISTS(opportunity_team ... deleted=0) AND owner_user_id <> 当前用户} 14 MINE 互斥</li>
* <li>{@link #FOLLOWED} 我关注的 {@code opportunity_focus} 命中当前用户 10/14</li>
* <li>{@link #MANAGE} 商机管理 {@code @DataScope} 部门天花板复用 crm-auth独立 workspace</li>
* <li>{@link #PUBLIC_POOL} 商机公海 {@code opp_status=1 owner_user_id } 逻辑视图 05独立 workspace</li>
* </ul>
*
* <p>个人四视图RECENT/MINE/PARTICIPATED/FOLLOWED叠加团队成员白名单可见性MANAGE/PUBLIC_POOL 严格
* DataScope 不混白名单PRD §1.3 生效边界前端以枚举名大写传入/未知值回落 {@link #DEFAULT}</p>
*/
public enum OpportunityViewType {
RECENT,
MINE,
PARTICIPATED,
FOLLOWED,
MANAGE,
PUBLIC_POOL;
/** 默认视图:无 viewType 或非法值回落「我负责的」(票 14 定义 3:默认视图未设 → mine)。 */
public static final OpportunityViewType DEFAULT = MINE;
/** 把前端字符串解析为视图类型;空白或无法识别回落 {@link #DEFAULT}。 */
public static OpportunityViewType fromValue(String value) {
if (StrUtil.isBlank(value)) {
return DEFAULT;
}
for (OpportunityViewType type : values()) {
if (type.name().equals(value)) {
return type;
}
}
return DEFAULT;
}
}

59
crm-opportunity/src/main/java/com/crm/opportunity/domain/param/OpportunityPageParam.java

@ -0,0 +1,59 @@
package com.crm.opportunity.domain.param;
import com.crm.base.domain.param.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 商机分页查询参数 10 四视图 + 14 视图族
*
* <p>{@code viewType} 区分视图数据集取值 {@link com.crm.opportunity.domain.enums.OpportunityViewType}
* /非法回落 MINE其余字段 = 列表可筛选列 14 确认点 3字段池自定义视图 filter_json 复用同一批列
* {@code @DataScope}{@code Opportunity} 实体注解经拦截器对 wrapper 自动追加部门天花板</p>
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OpportunityPageParam extends BaseParam {
@Schema(description = "视图类型(RECENT/MINE/PARTICIPATED/FOLLOWED/MANAGE/PUBLIC_POOL,空/非法回落 MINE)")
private String viewType;
@Schema(description = "商机状态筛选(多值,支撑复合卡片下钻)")
private List<Integer> statusIn;
@Schema(description = "商机来源字典 code 筛选")
private String oppSource;
@Schema(description = "商机类型字典 code 筛选")
private String oppType;
@Schema(description = "行业字典 code 筛选")
private String industryCode;
@Schema(description = "招标形式字典 code 筛选")
private String bidForm;
@Schema(description = "项目属地字典 code 筛选")
private String localityType;
@Schema(description = "省份国标 code 筛选")
private String provinceCode;
@Schema(description = "市/区国标 code 筛选")
private String cityCode;
@Schema(description = "归属部门ID筛选")
private Long ownerDeptId;
@Schema(description = "负责人ID筛选")
private Long ownerUserId;
@Schema(description = "进入公海原因字典 code 筛选(公海视图)")
private String poolReason;
@Schema(description = "当前阶段节点ID筛选")
private Long currentStageId;
}

73
crm-opportunity/src/test/java/com/crm/opportunity/query/OpportunityViewQuerySkeletonTest.java

@ -0,0 +1,73 @@
package com.crm.opportunity.query;
import com.crm.opportunity.domain.dto.OpportunityDTO;
import com.crm.opportunity.domain.entity.Opportunity;
import com.crm.opportunity.domain.enums.OpportunityViewType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 商机读侧骨架单测 10视图类型解析 + DTO 双向转换
*/
@DisplayName("商机读侧骨架 ViewType + DTO(票 10)")
class OpportunityViewQuerySkeletonTest {
@Test
@DisplayName("OpportunityViewType 六视图 + 默认 MINE")
void viewType_domain() {
assertThat(OpportunityViewType.values()).hasSize(6);
assertThat(OpportunityViewType.DEFAULT).isEqualTo(OpportunityViewType.MINE);
}
@Test
@DisplayName("fromValue:命中大写枚举名,空/非法回落 MINE")
void viewType_fromValue() {
assertThat(OpportunityViewType.fromValue("PARTICIPATED"))
.isEqualTo(OpportunityViewType.PARTICIPATED);
assertThat(OpportunityViewType.fromValue("PUBLIC_POOL"))
.isEqualTo(OpportunityViewType.PUBLIC_POOL);
assertThat(OpportunityViewType.fromValue(null)).isEqualTo(OpportunityViewType.MINE);
assertThat(OpportunityViewType.fromValue(" ")).isEqualTo(OpportunityViewType.MINE);
assertThat(OpportunityViewType.fromValue("nope")).isEqualTo(OpportunityViewType.MINE);
}
@Test
@DisplayName("DTO fromEntity 拷主表字段,回显/计算列不来自 entity")
void dto_fromEntity_copiesBaseFields() {
Opportunity e = new Opportunity();
e.setOppName("XX智能化项目");
e.setOwnerUserId(1001L);
e.setOppStatus(2);
e.setProjectAmount(new BigDecimal("128000.00"));
e.setPrimaryCustomerNameSnapshot("上海XX公司");
OpportunityDTO dto = OpportunityDTO.fromEntity(e);
assertThat(dto.getOppName()).isEqualTo("XX智能化项目");
assertThat(dto.getOwnerUserId()).isEqualTo(1001L);
assertThat(dto.getOppStatus()).isEqualTo(2);
assertThat(dto.getProjectAmount()).isEqualByComparingTo("128000.00");
assertThat(dto.getPrimaryCustomerNameSnapshot()).isEqualTo("上海XX公司");
// 计算列/回显未填充
assertThat(dto.getStageStayDays()).isNull();
assertThat(dto.getSchemeCardStatus()).isNull();
assertThat(dto.getFollowed()).isNull();
}
@Test
@DisplayName("DTO toEntity 反向拷回")
void dto_toEntity_roundTrip() {
OpportunityDTO dto = new OpportunityDTO();
dto.setOppName("回写商机");
dto.setOppStatus(3);
Opportunity e = dto.toEntity();
assertThat(e.getOppName()).isEqualTo("回写商机");
assertThat(e.getOppStatus()).isEqualTo(3);
}
}
Loading…
Cancel
Save