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, "仅草稿版本可删除");