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