Browse Source
植入一条全公司通用发布中默认阶段模板作转商机阶段落位兜底: 客户圈定→关系摸排→资料采集→方案卡→已转项目(引用 opp_stage 字典 01-05), 末节点已转项目固定(is_fixed=1,票06契约)。对称方案卡 SchemeFieldDefInitializer 兜底范式,幂等按 code 判存(含已停用不复活)。零 A4/零字典改动。crm-rule 131 全绿。master
3 changed files with 226 additions and 0 deletions
@ -0,0 +1,96 @@ |
|||
package com.crm.rule.config; |
|||
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.crm.rule.constant.OpportunityRuleConstants; |
|||
import com.crm.rule.domain.entity.OpportunityStageNode; |
|||
import com.crm.rule.domain.entity.OpportunityStageTemplate; |
|||
import com.crm.rule.mapper.OpportunityStageNodeMapper; |
|||
import com.crm.rule.mapper.OpportunityStageTemplateMapper; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.boot.CommandLineRunner; |
|||
import org.springframework.core.annotation.Order; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 全局默认阶段模板兜底种子化(票 15 / 票 02 阶段落位兜底 / PRD §6) |
|||
* |
|||
* <p>初始化植入一条「全公司通用」发布中默认阶段模板,作转商机时 |
|||
* {@code IOpportunityStageTemplateService.resolveBindingVersion} 的命中兜底 |
|||
* (对称 {@link SchemeFieldDefInitializer} 的方案卡兜底模板范式):无此兜底则转商机首个商机 |
|||
* 的 {@code current_stage_id} 落空(票 16 走法 A 容忍空),有则常态命中默认模板。</p> |
|||
* |
|||
* <p>默认阶段轨道(产品拍板 20260826):客户圈定 → 关系摸排 → 资料采集 → 方案卡 → 已转项目 |
|||
* (引用 {@code opp_stage} 字典 OPP_STAGE_01..05,字典项由字典域种子化,本处仅组装模板版本行)。 |
|||
* 末节点「已转项目」为固定节点({@code is_fixed=1},票 06 契约:不可删、仅方案卡转项目驱动到达)。</p> |
|||
* |
|||
* <p>幂等键 = 模板编码:同 code 行存在(含已停用)即跳过,管理员停用兜底后重启不复活。</p> |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
@Order(17) |
|||
@RequiredArgsConstructor |
|||
public class StageTemplateInitializer implements CommandLineRunner { |
|||
|
|||
private final OpportunityStageTemplateMapper templateMapper; |
|||
private final OpportunityStageNodeMapper nodeMapper; |
|||
|
|||
@Override |
|||
public void run(String... args) { |
|||
Long existing = templateMapper.selectCount( |
|||
new LambdaQueryWrapper<OpportunityStageTemplate>() |
|||
.eq(OpportunityStageTemplate::getTemplateCode, |
|||
OpportunityRuleConstants.SEED_STAGE_TPL_CODE)); |
|||
if (existing != null && existing > 0) { |
|||
log.info("默认阶段模板已存在(code={},含已停用),跳过种子化", |
|||
OpportunityRuleConstants.SEED_STAGE_TPL_CODE); |
|||
return; |
|||
} |
|||
|
|||
OpportunityStageTemplate template = buildSeedTemplate(); |
|||
templateMapper.insert(template); |
|||
List<OpportunityStageNode> nodes = buildSeedNodes(template.getId()); |
|||
nodes.forEach(nodeMapper::insert); |
|||
log.info("已植入全局默认阶段模板({},发布中/全公司通用/默认,{} 个节点)", |
|||
template.getTemplateCode(), nodes.size()); |
|||
} |
|||
|
|||
/** 兜底模板头(包级可见供测试断言):固定编码 V1.0 直发发布中、全公司通用且默认 */ |
|||
OpportunityStageTemplate buildSeedTemplate() { |
|||
OpportunityStageTemplate template = new OpportunityStageTemplate(); |
|||
template.setTemplateCode(OpportunityRuleConstants.SEED_STAGE_TPL_CODE); |
|||
template.setTemplateName(OpportunityRuleConstants.SEED_STAGE_TPL_NAME); |
|||
template.setVersionNo("V1.0"); |
|||
template.setStatus(OpportunityRuleConstants.VERSION_STATUS_PUBLISHED); |
|||
template.setApplyScope(OpportunityRuleConstants.APPLY_SCOPE_ALL); |
|||
template.setIsDefault(OpportunityRuleConstants.FLAG_YES); |
|||
template.setTemplateDesc("系统初始化种子模板(客户圈定→关系摸排→资料采集→方案卡→已转项目)," |
|||
+ "全公司通用兜底;可停用或发新版替换"); |
|||
return template; |
|||
} |
|||
|
|||
/** |
|||
* 兜底模板 5 节点(包级可见供测试断言):seq 1-5 依序引用 opp_stage 字典 01-05, |
|||
* custom_node_name/work_goal 留空(带入字典名),末节点「已转项目」固定。 |
|||
*/ |
|||
List<OpportunityStageNode> buildSeedNodes(Long templateId) { |
|||
return List.of( |
|||
seedNode(templateId, 1, "OPP_STAGE_01", OpportunityRuleConstants.FLAG_NO), |
|||
seedNode(templateId, 2, "OPP_STAGE_02", OpportunityRuleConstants.FLAG_NO), |
|||
seedNode(templateId, 3, "OPP_STAGE_03", OpportunityRuleConstants.FLAG_NO), |
|||
seedNode(templateId, 4, "OPP_STAGE_04", OpportunityRuleConstants.FLAG_NO), |
|||
seedNode(templateId, 5, OpportunityRuleConstants.FIXED_STAGE_DICT_CODE, |
|||
OpportunityRuleConstants.FLAG_YES)); |
|||
} |
|||
|
|||
private OpportunityStageNode seedNode(Long templateId, int seqNo, String stageDictCode, int isFixed) { |
|||
OpportunityStageNode node = new OpportunityStageNode(); |
|||
node.setTemplateId(templateId); |
|||
node.setSeqNo(seqNo); |
|||
node.setStageDictCode(stageDictCode); |
|||
node.setIsFixed(isFixed); |
|||
return node; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
package com.crm.rule.config; |
|||
|
|||
import com.baomidou.mybatisplus.core.MybatisConfiguration; |
|||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
|||
import com.crm.rule.constant.OpportunityRuleConstants; |
|||
import com.crm.rule.domain.entity.OpportunityStageNode; |
|||
import com.crm.rule.domain.entity.OpportunityStageTemplate; |
|||
import com.crm.rule.mapper.OpportunityStageNodeMapper; |
|||
import com.crm.rule.mapper.OpportunityStageTemplateMapper; |
|||
import org.apache.ibatis.builder.MapperBuilderAssistant; |
|||
import org.junit.jupiter.api.BeforeAll; |
|||
import org.junit.jupiter.api.DisplayName; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.ArgumentCaptor; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
|
|||
import java.util.List; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.Mockito.doAnswer; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
/** |
|||
* 全局默认阶段模板兜底种子化规格验证(票 15 / 票 02 阶段落位兜底)。 |
|||
* |
|||
* <p>断言点(对称 {@link SchemeFieldDefInitializerTest}):</p> |
|||
* <ul> |
|||
* <li>首装:植入 1 头(固定编码 V1.0/发布中/全公司通用/默认)+ 5 节点 |
|||
* (客户圈定→关系摸排→资料采集→方案卡→已转项目,seq 1-5,末节点 is_fixed=1 且 dictCode=OPP_STAGE_05)</li> |
|||
* <li>幂等:模板按编码判存(含已停用不复活——管理员停用兜底后重启不重建)</li> |
|||
* </ul> |
|||
*/ |
|||
@DisplayName("全局默认阶段模板兜底种子化(票 15)") |
|||
@ExtendWith(MockitoExtension.class) |
|||
class StageTemplateInitializerTest { |
|||
|
|||
private static final Long SEED_TPL_ROW_ID = 8801L; |
|||
|
|||
@Mock private OpportunityStageTemplateMapper templateMapper; |
|||
@Mock private OpportunityStageNodeMapper nodeMapper; |
|||
|
|||
@InjectMocks |
|||
private StageTemplateInitializer initializer; |
|||
|
|||
@BeforeAll |
|||
static void initLambdaCache() { |
|||
MapperBuilderAssistant assistant = |
|||
new MapperBuilderAssistant(new MybatisConfiguration(), ""); |
|||
TableInfoHelper.initTableInfo(assistant, OpportunityStageTemplate.class); |
|||
TableInfoHelper.initTableInfo(assistant, OpportunityStageNode.class); |
|||
} |
|||
|
|||
/** 首装桩:无同码模板 + insert 回填模板行 id(模拟 ASSIGN_ID 雪花回填) */ |
|||
private void stubFirstRun() { |
|||
when(templateMapper.selectCount(any())).thenReturn(0L); |
|||
doAnswer(inv -> { |
|||
OpportunityStageTemplate tpl = inv.getArgument(0); |
|||
tpl.setId(SEED_TPL_ROW_ID); |
|||
return 1; |
|||
}).when(templateMapper).insert(any(OpportunityStageTemplate.class)); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("首装:种 1 头(发布中/全公司通用/默认/V1.0/固定编码)+ 5 节点(末节点固定=已转项目)") |
|||
void firstRun_seedsDefaultStageTemplate() { |
|||
stubFirstRun(); |
|||
initializer.run(); |
|||
|
|||
// 模板头:固定编码、V1.0 直发发布中、全公司通用且默认
|
|||
ArgumentCaptor<OpportunityStageTemplate> tplCaptor = |
|||
ArgumentCaptor.forClass(OpportunityStageTemplate.class); |
|||
verify(templateMapper).insert(tplCaptor.capture()); |
|||
OpportunityStageTemplate template = tplCaptor.getValue(); |
|||
assertThat(template.getTemplateCode()).isEqualTo(OpportunityRuleConstants.SEED_STAGE_TPL_CODE) |
|||
.isEqualTo("OPP_STAGE_TPL_01"); |
|||
assertThat(template.getTemplateName()).isEqualTo(OpportunityRuleConstants.SEED_STAGE_TPL_NAME); |
|||
assertThat(template.getVersionNo()).isEqualTo("V1.0"); |
|||
assertThat(template.getStatus()).isEqualTo(OpportunityRuleConstants.VERSION_STATUS_PUBLISHED); |
|||
assertThat(template.getApplyScope()).isEqualTo(OpportunityRuleConstants.APPLY_SCOPE_ALL); |
|||
assertThat(template.getIsDefault()).isEqualTo(OpportunityRuleConstants.FLAG_YES); |
|||
|
|||
// 节点:5 条挂模板行 id、seq 1-5、字典 code 依序、末节点固定
|
|||
ArgumentCaptor<OpportunityStageNode> nodeCaptor = |
|||
ArgumentCaptor.forClass(OpportunityStageNode.class); |
|||
verify(nodeMapper, times(5)).insert(nodeCaptor.capture()); |
|||
List<OpportunityStageNode> nodes = nodeCaptor.getAllValues(); |
|||
assertThat(nodes).allMatch(n -> SEED_TPL_ROW_ID.equals(n.getTemplateId())); |
|||
assertThat(nodes).extracting(OpportunityStageNode::getSeqNo) |
|||
.containsExactly(1, 2, 3, 4, 5); |
|||
assertThat(nodes).extracting(OpportunityStageNode::getStageDictCode) |
|||
.containsExactly("OPP_STAGE_01", "OPP_STAGE_02", "OPP_STAGE_03", "OPP_STAGE_04", "OPP_STAGE_05"); |
|||
|
|||
// 前 4 节点普通、末节点固定(is_fixed=1 且 dictCode=OPP_STAGE_05,票 06 契约)
|
|||
assertThat(nodes.subList(0, 4)).allMatch(n -> Integer.valueOf(0).equals(n.getIsFixed())); |
|||
OpportunityStageNode last = nodes.get(4); |
|||
assertThat(last.getIsFixed()).isEqualTo(OpportunityRuleConstants.FLAG_YES); |
|||
assertThat(last.getStageDictCode()).isEqualTo(OpportunityRuleConstants.FIXED_STAGE_DICT_CODE); |
|||
} |
|||
|
|||
@Test |
|||
@DisplayName("幂等重跑:模板按编码判存(含已停用)不复活,插入总数不变") |
|||
void rerun_idempotent() { |
|||
stubFirstRun(); |
|||
initializer.run(); |
|||
|
|||
// 二跑:同码模板行已存在(任意状态,如管理员已停用)
|
|||
when(templateMapper.selectCount(any())).thenReturn(1L); |
|||
initializer.run(); |
|||
|
|||
// 插入总数不变:模板 1、节点 5(二跑各补 0)
|
|||
verify(templateMapper, times(1)).insert(any(OpportunityStageTemplate.class)); |
|||
verify(nodeMapper, times(5)).insert(any(OpportunityStageNode.class)); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue