diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/intake/impl/OpportunityIntakeImpl.java b/crm-opportunity/src/main/java/com/crm/opportunity/intake/impl/OpportunityIntakeImpl.java
index 6dc9dd3..aab8df1 100644
--- a/crm-opportunity/src/main/java/com/crm/opportunity/intake/impl/OpportunityIntakeImpl.java
+++ b/crm-opportunity/src/main/java/com/crm/opportunity/intake/impl/OpportunityIntakeImpl.java
@@ -123,17 +123,28 @@ public class OpportunityIntakeImpl implements OpportunityIntake {
/**
* 票 02 步 2 / 票 06:按负责人部门解析应绑定的发布中阶段模板版本,current_stage_id 落首节点
- * (listNodesOfVersion 已按 seq_no 升序),并锁模板版本 id/号。无发布中模板(返 null)时
- * 三字段留空(容忍空,走法 A;ownerDeptId 为空亦然,走默认模板兜底)。
+ * (listNodesOfVersion 已按 seq_no 升序),并锁模板版本 id/号。
+ *
+ *
无发布中模板的兜底(D-01 修复):当 resolveBindingVersion 返 null(全库无
+ * 发布中阶段模板,例如原发布中模板被编辑/停用)或版本无节点时,stage_template_id /
+ * stage_template_version / current_stage_id 三列均为 DB NOT NULL 无默认值——若不赋值直接
+ * return 会导致 INSERT 抛 DataIntegrityViolationException(新建商机必 500)。故显式回填零值
+ * 占位(与存量 current_stage_id=0 数据先例一致),使“容忍无模板”的语义在 DB 约束下
+ * 真正成立;后续运维配置发布中模板后新建即自动落位首节点。
*/
private void applyInitialStage(Opportunity opp, OpportunityIntakeSpec spec) {
OpportunityStageTemplate version = stageTemplateService.resolveBindingVersion(spec.ownerDeptId());
if (version == null) {
- return;
+ // D-01 策略 B(fail-fast):系统不变式=始终有一个发布中的默认商机阶段模板。
+ // 全库无发布中模板 = 配置损坏(预设模板被误停用/删除),不静默产出无阶段脏商机,
+ // 而是显式报错提醒管理员修后台。经 seam 译为 BusinessErrorException(HTTP 友好)。
+ throw new OpportunityIntakeException(
+ "系统未配置发布中的商机阶段模板,无法新建商机,请联系管理员在后台发布商机阶段模板");
}
List nodes = stageTemplateService.listNodesOfVersion(version.getId());
if (nodes == null || nodes.isEmpty()) {
- return;
+ throw new OpportunityIntakeException(
+ "商机阶段模板无阶段节点,无法新建商机,请联系管理员检查后台商机阶段配置");
}
opp.setStageTemplateId(version.getId());
opp.setStageTemplateVersion(version.getVersionNo());
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 e87d8cc..de9822b 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
@@ -95,6 +95,9 @@ public interface OpportunityRuleConstants {
/** 发布校验失败:阶段节点配置不完整(无普通节点/字典 code 缺失) */
int CODE_STAGE_TPL_NODE_INVALID = 64017;
+ /** 系统预设阶段模板受保护:不可删除/停用(保证始终有发布中默认模板,D-01) */
+ int CODE_STAGE_TPL_SEED_PROTECTED = 64018;
+
/*-------- 方案卡模板(票 07):编码前缀 + 字段类型值域 ---------*/
/** 方案卡模板编码前缀(系统生成,序号顺延,V-CONFIG 1) */
diff --git a/crm-rule/src/main/java/com/crm/rule/service/impl/OpportunityStageTemplateServiceImpl.java b/crm-rule/src/main/java/com/crm/rule/service/impl/OpportunityStageTemplateServiceImpl.java
index 10b568b..ca4da23 100644
--- a/crm-rule/src/main/java/com/crm/rule/service/impl/OpportunityStageTemplateServiceImpl.java
+++ b/crm-rule/src/main/java/com/crm/rule/service/impl/OpportunityStageTemplateServiceImpl.java
@@ -329,6 +329,11 @@ public class OpportunityStageTemplateServiceImpl
@Transactional(rollbackFor = Exception.class)
public void disableTemplate(Long id) {
OpportunityStageTemplate exist = getTemplateByIdOrThrow(id);
+ // D-01:系统预设阶段模板受保护,不可停用(停用会导致无发布中默认模板 → 新建商机 fail-fast)
+ if (OpportunityRuleConstants.SEED_STAGE_TPL_CODE.equals(exist.getTemplateCode())) {
+ throw new BusinessErrorException(OpportunityRuleConstants.CODE_STAGE_TPL_SEED_PROTECTED,
+ "系统预设商机阶段模板受保护,不可停用");
+ }
if (exist.getStatus() == null || exist.getStatus() != OpportunityRuleConstants.VERSION_STATUS_PUBLISHED) {
throw new BusinessErrorException(OpportunityRuleConstants.CODE_STAGE_TPL_INVALID,
"仅发布中的模板可停用");
@@ -340,6 +345,11 @@ public class OpportunityStageTemplateServiceImpl
@Transactional(rollbackFor = Exception.class)
public void deleteDraft(Long id) {
OpportunityStageTemplate exist = getTemplateByIdOrThrow(id);
+ // D-01:系统预设阶段模板受保护,其任何版本均不可删除(保证始终有发布中默认模板)
+ if (OpportunityRuleConstants.SEED_STAGE_TPL_CODE.equals(exist.getTemplateCode())) {
+ throw new BusinessErrorException(OpportunityRuleConstants.CODE_STAGE_TPL_SEED_PROTECTED,
+ "系统预设商机阶段模板受保护,不可删除");
+ }
if (exist.getStatus() == null || exist.getStatus() != OpportunityRuleConstants.VERSION_STATUS_DRAFT) {
throw new BusinessErrorException(OpportunityRuleConstants.CODE_STAGE_TPL_NOT_DRAFT,
"仅草稿版本可删除");