From 8ddc54143c9e67fe7ed43417d8c4046f93b21e7e Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Tue, 25 Aug 2026 16:14:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(rule):=20=E7=A5=A815=20=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E9=98=B6=E6=AE=B5=E6=A8=A1=E6=9D=BF=E5=85=9C?= =?UTF-8?q?=E5=BA=95=20seed=EF=BC=88StageTemplateInitializer=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 植入一条全公司通用发布中默认阶段模板作转商机阶段落位兜底: 客户圈定→关系摸排→资料采集→方案卡→已转项目(引用 opp_stage 字典 01-05), 末节点已转项目固定(is_fixed=1,票06契约)。对称方案卡 SchemeFieldDefInitializer 兜底范式,幂等按 code 判存(含已停用不复活)。零 A4/零字典改动。crm-rule 131 全绿。 --- .../rule/config/StageTemplateInitializer.java | 96 ++++++++++++++ .../constant/OpportunityRuleConstants.java | 10 ++ .../config/StageTemplateInitializerTest.java | 120 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 crm-rule/src/main/java/com/crm/rule/config/StageTemplateInitializer.java create mode 100644 crm-rule/src/test/java/com/crm/rule/config/StageTemplateInitializerTest.java diff --git a/crm-rule/src/main/java/com/crm/rule/config/StageTemplateInitializer.java b/crm-rule/src/main/java/com/crm/rule/config/StageTemplateInitializer.java new file mode 100644 index 0000000..794f5d7 --- /dev/null +++ b/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) + * + *

初始化植入一条「全公司通用」发布中默认阶段模板,作转商机时 + * {@code IOpportunityStageTemplateService.resolveBindingVersion} 的命中兜底 + * (对称 {@link SchemeFieldDefInitializer} 的方案卡兜底模板范式):无此兜底则转商机首个商机 + * 的 {@code current_stage_id} 落空(票 16 走法 A 容忍空),有则常态命中默认模板。

+ * + *

默认阶段轨道(产品拍板 20260826):客户圈定 → 关系摸排 → 资料采集 → 方案卡 → 已转项目 + * (引用 {@code opp_stage} 字典 OPP_STAGE_01..05,字典项由字典域种子化,本处仅组装模板版本行)。 + * 末节点「已转项目」为固定节点({@code is_fixed=1},票 06 契约:不可删、仅方案卡转项目驱动到达)。

+ * + *

幂等键 = 模板编码:同 code 行存在(含已停用)即跳过,管理员停用兜底后重启不复活。

+ */ +@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() + .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 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 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; + } +} diff --git a/crm-rule/src/main/java/com/crm/rule/constant/OpportunityRuleConstants.java b/crm-rule/src/main/java/com/crm/rule/constant/OpportunityRuleConstants.java index 7484023..e87d8cc 100644 --- a/crm-rule/src/main/java/com/crm/rule/constant/OpportunityRuleConstants.java +++ b/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 顺延) --------*/ /** 规则参数/状态校验失败 */ diff --git a/crm-rule/src/test/java/com/crm/rule/config/StageTemplateInitializerTest.java b/crm-rule/src/test/java/com/crm/rule/config/StageTemplateInitializerTest.java new file mode 100644 index 0000000..85c88fe --- /dev/null +++ b/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 阶段落位兜底)。 + * + *

断言点(对称 {@link SchemeFieldDefInitializerTest}):

+ *
    + *
  • 首装:植入 1 头(固定编码 V1.0/发布中/全公司通用/默认)+ 5 节点 + * (客户圈定→关系摸排→资料采集→方案卡→已转项目,seq 1-5,末节点 is_fixed=1 且 dictCode=OPP_STAGE_05)
  • + *
  • 幂等:模板按编码判存(含已停用不复活——管理员停用兜底后重启不重建)
  • + *
+ */ +@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 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 nodeCaptor = + ArgumentCaptor.forClass(OpportunityStageNode.class); + verify(nodeMapper, times(5)).insert(nodeCaptor.capture()); + List 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)); + } +}