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));
+ }
+}