From 938aed922e95c44031f0e75d587005e10898d467 Mon Sep 17 00:00:00 2001
From: luoweijian <1329394916@qq.com>
Date: Thu, 27 Aug 2026 09:17:56 +0800
Subject: [PATCH] =?UTF-8?q?fix(opportunity):=20=E4=BF=AE=E5=A4=8D=E6=96=B0?=
=?UTF-8?q?=E5=BB=BA=E5=95=86=E6=9C=BA=20500=20=E5=B9=B6=E4=BF=9D=E6=8A=A4?=
=?UTF-8?q?=E7=B3=BB=E7=BB=9F=E9=A2=84=E8=AE=BE=E9=98=B6=E6=AE=B5=E6=A8=A1?=
=?UTF-8?q?=E6=9D=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新建商机因 current_stage_id/stage_template_id/stage_template_version
三列 NOT NULL 无默认值、而无发布中默认阶段模板时静默 return 而崩(500)。
- OpportunityIntakeImpl.applyInitialStage 改 fail-fast:无发布中默认模板
或版本无节点时抛 OpportunityIntakeException(经 seam 译为 HTTP 友好
BusinessErrorException),不再静默产出无阶段脏商机
- 后台阶段配置保护系统预设模板(SEED_STAGE_TPL_CODE=OPP_STAGE_TPL_01):
deleteDraft/disableTemplate 拒绝删除/停用,保证始终有发布中默认模板
- 新增错误码 CODE_STAGE_TPL_SEED_PROTECTED=64018
---
.../intake/impl/OpportunityIntakeImpl.java | 19 +++++++++++++++----
.../constant/OpportunityRuleConstants.java | 3 +++
.../OpportunityStageTemplateServiceImpl.java | 10 ++++++++++
3 files changed, 28 insertions(+), 4 deletions(-)
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,
"仅草稿版本可删除");