Browse Source

feat(rule): 票15 全局默认阶段模板兜底 seed(StageTemplateInitializer)

植入一条全公司通用发布中默认阶段模板作转商机阶段落位兜底:
客户圈定→关系摸排→资料采集→方案卡→已转项目(引用 opp_stage 字典 01-05),
末节点已转项目固定(is_fixed=1,票06契约)。对称方案卡 SchemeFieldDefInitializer
兜底范式,幂等按 code 判存(含已停用不复活)。零 A4/零字典改动。crm-rule 131 全绿。
master
luoweijian 2 weeks ago
parent
commit
8ddc54143c
  1. 96
      crm-rule/src/main/java/com/crm/rule/config/StageTemplateInitializer.java
  2. 10
      crm-rule/src/main/java/com/crm/rule/constant/OpportunityRuleConstants.java
  3. 120
      crm-rule/src/test/java/com/crm/rule/config/StageTemplateInitializerTest.java

96
crm-rule/src/main/java/com/crm/rule/config/StageTemplateInitializer.java

@ -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;
}
}

10
crm-rule/src/main/java/com/crm/rule/constant/OpportunityRuleConstants.java

@ -51,6 +51,16 @@ public interface OpportunityRuleConstants {
/** 「已转项目」固定节点展示名(custom_node_name 为空时的字典名兜底) */
String FIXED_STAGE_NAME = "已转项目";
/**
* 种子兜底阶段模板编码 15 / 02初始化植入全公司通用发布中默认模板作转商机阶段落位兜底
* 固定 01 nextTemplateCode 顺延号兼容首装空表时种子先持 01后续新建从 02
* 对称方案卡 {@link #SEED_SCHEME_TPL_CODE}
*/
String SEED_STAGE_TPL_CODE = STAGE_TPL_CODE_PREFIX + "01";
/** 种子兜底阶段模板名称 */
String SEED_STAGE_TPL_NAME = "全公司通用阶段模板";
/*-------- 业务错误码(64xxx,接 RuleConstants 64007 顺延) --------*/
/** 规则参数/状态校验失败 */

120
crm-rule/src/test/java/com/crm/rule/config/StageTemplateInitializerTest.java

@ -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…
Cancel
Save