去年我们团队接手了一个复杂的保险理赔系统,其中有一个"巨无霸"理赔流程------包含了37个审批节点、15条分支路径和23种异常处理。每次业务规则变更,都需要开发团队修改BPMN流程图,测试团队进行全流程回归,发布时间从1周延长到1个月。直到我们发现了CMMN这个"隐藏的宝石"。
一、噩梦般的保险理赔流程
最初的理赔系统采用传统的BPMN流程,长这样:
XML
<!-- 传统的BPMN理赔流程 - 部分代码 -->
<process id="claimProcess">
<!-- 37个用户任务节点 -->
<userTask id="receiveClaim" name="接收理赔申请"/>
<userTask id="validateDocuments" name="验证资料完整性"/>
<userTask id="assessDamage" name="损失评估"/>
<!-- ... 中间省略34个节点 ... -->
<userTask id="finalApproval" name="最终审批"/>
<!-- 15条分支路径 -->
<exclusiveGateway id="damageAssessmentGateway">
<sequenceFlow sourceRef="assessDamage" targetRef="smallClaim">
<conditionExpression>x${amount < 5000}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="assessDamage" targetRef="mediumClaim">
<conditionExpression>x${amount >= 5000 && amount < 50000}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="assessDamage" targetRef="largeClaim">
<conditionExpression>x${amount >= 50000}</conditionExpression>
</sequenceFlow>
</exclusiveGateway>
<!-- 异常处理流程 -->
<boundaryEvent id="timeoutEvent" attachedToRef="assessDamage">
<timerEventDefinition>
<timeDuration>PT24H</timeDuration>
</timerEventDefinition>
</boundaryEvent>
</process>
痛点分析:
- 刚性流程:每个理赔案件必须按照预定路径执行
- 变更成本高:增加一个新的资料类型需要修改整个流程
- 无法应对异常:遇到特殊情况需要"走特殊流程"时系统无法支持
- 监控困难:案件卡在哪个环节?为什么卡住?很难追踪
业务总监的原话是:"我们的理赔处理像在走迷宫,明明有些案件很简单,却要跟复杂案件走一样的流程。"
二、技术选型:为什么是CMMN?
备选方案对比
我们考虑了三种方案:
方案A:继续优化BPMN流程
- 优点:团队熟悉,开发速度快
- 缺点:无法解决根本的流程刚性問題
方案B:自定义状态机
java
// 自研状态机方案
public class ClaimStateMachine {
private Map<ClaimState, List<ClaimEvent>> transitions = new HashMap<>();
public void addTransition(ClaimState from, ClaimEvent event, ClaimState to) {
// 自定义状态转移逻辑
}
// 问题:需要自己实现持久化、回退、监控等全套功能
}
方案C:Flowable CMMN
- 基于案例管理模型,适合非结构化流程
- 支持动态任务激活和完成
- 内置持久化和监控能力
决策关键点:
java
// CMMN的核心概念验证
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("insuranceClaim")
.variable("claimType", "auto") // 车辆理赔
.variable("amount", 15000) // 金额1.5万
.start();
// 系统会根据案件情况动态激活不同的任务
// 而不是按照预定路径执行
三、架构重构:从流程驱动到案例驱动
第一阶段:CMMN案例模型设计
XML
<!-- insurance-claim.cmmn -->
<case id="insuranceClaim" name="保险理赔案例">
<!-- 案例参数 -->
<casePlanModel id="casePlanModel">
<!-- 第一阶段:必须完成的任务 -->
<planItem id="receiveClaim" definitionRef="receiveClaimTask"/>
<planItem id="validateDocuments" definitionRef="validateDocumentsTask"/>
<!-- 条件性任务:只有金额大于1万才需要评估 -->
<planItem id="assessDamage" definitionRef="assessDamageTask">
<entryCriterion sentryRef="amountOver10kSentry"/>
</PlanItem>
<!-- 并行任务:调查和评估可以同时进行 -->
<planItem id="investigate" definitionRef="investigateTask"/>
<planItem id="evaluate" definitionRef="evaluateTask"/>
<!-- 哨兵(Sentry):定义任务激活条件 -->
<sentry id="amountOver10kSentry">
<planItemOnPart sourceRef="validateDocuments">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<condition>${amount > 10000}</condition>
</sentry>
</casePlanModel>
<!-- 任务定义 -->
<humanTask id="receiveClaimTask" name="接收理赔申请"/>
<humanTask id="validateDocumentsTask" name="验证资料完整性"/>
<humanTask id="assessDamageTask" name="损失评估"/>
</case>
第二阶段:动态任务管理
java
@Service
public class DynamicClaimService {
@Autowired
private CmmnRuntimeService cmmnRuntimeService;
@Autowired
private CmmnTaskService cmmnTaskService;
// 根据案件情况动态添加任务
public void addAdditionalTask(String caseInstanceId, String taskType) {
// 动态创建计划项
PlanItemInstance planItemInstance = cmmnRuntimeService
.createPlanItemInstanceBuilder()
.caseInstanceId(caseInstanceId)
.planItemDefinitionId(taskType)
.add();
// 手动启动任务
cmmnRuntimeService.startPlanItemInstance(planItemInstance.getId());
}
// 处理复杂理赔案件
public void handleComplexClaim(String caseInstanceId, Claim claim) {
// 根据案件复杂度动态添加任务
if (claim.hasSuspiciousPattern()) {
addAdditionalTask(caseInstanceId, "fraudInvestigation");
}
if (claim.requiresExpertOpinion()) {
addAdditionalTask(caseInstanceId, "expertReview");
}
// 任务可以并行执行,不需要等待前序任务完成
}
}
第三阶段:事件驱动的案件处理
java
@Component
public class ClaimEventListener {
@EventListener
public void handleTaskCompleted(CmmnTaskCompletedEvent event) {
String caseInstanceId = event.getCaseInstanceId();
String taskId = event.getTaskId();
// 任务完成时自动触发后续动作
if ("validateDocuments".equals(taskId)) {
// 根据验证结果决定下一步
Boolean documentsValid = (Boolean) event.getVariable("documentsValid");
if (Boolean.TRUE.equals(documentsValid)) {
// 自动激活评估任务
activateAssessmentTasks(caseInstanceId);
} else {
// 请求补充材料
requestAdditionalDocuments(caseInstanceId);
}
}
}
private void activateAssessmentTasks(String caseInstanceId) {
// 查询案件信息
CaseInstance caseInstance = cmmnRuntimeService
.createCaseInstanceQuery()
.caseInstanceId(caseInstanceId)
.singleResult();
Double amount = (Double) caseInstance.getVariable("amount");
if (amount > 50000) {
// 大额案件需要额外审批
cmmnRuntimeService.triggerPlanItemInstance("seniorApproval");
}
}
}
四、核心技术决策的深度思考
决策1:何时用BPMN,何时用CMMN?
我们制定了明确的选择标准:
java
public class ProcessSelectionFramework {
// 适合BPMN的场景
public boolean isSuitableForBPMN(ProcessRequirement req) {
return req.hasStrictSequence() || // 严格的顺序执行
req.hasWellDefinedOutcomes() || // 明确的结果
req.isRepeatableProcess(); // 可重复的流程
}
// 适合CMMN的场景
public boolean isSuitableForCMMN(CaseRequirement req) {
return req.hasUnpredictablePath() || // 不可预测的路径
req.requiresAdaptation() || // 需要适应性
req.hasKnowledgeWorkNature(); // 知识工作性质
}
// 实际案例:保险理赔适合CMMN
public void evaluateInsuranceClaim() {
CaseRequirement claimReq = new CaseRequirement();
claimReq.setUnpredictablePath(true); // 每个案件路径不同
claimReq.setAdaptationRequired(true); // 需要根据情况调整
claimReq.setKnowledgeWork(true); // 依赖核保员经验
assert isSuitableForCMMN(claimReq); // 返回true
}
}
决策2:任务粒度设计
错误示范:任务粒度过细
java
<!-- 反例:过度拆分的任务 -->
<humanTask id="validateDriverLicense" name="验证驾驶证"/>
<humanTask id="validateInsuranceCard" name="验证保险卡"/>
<humanTask id="validateAccidentReport" name="验证事故报告"/>
<!-- 用户需要点击十几次才能完成资料验证 -->
正确做法:合理的任务聚合
java
<!-- 正例:有意义的任务单元 -->
<humanTask id="validateDocuments" name="验证理赔资料">
<extensionElements>
<!-- 在表单中定义需要验证的所有资料 -->
<flowable:formProperty id="documents"
name="资料验证" type="document-list"/>
</extensionElements>
</humanTask>
决策3:哨兵条件的设计哲学
java
<!-- 简单的条件哨兵 -->
<sentry id="basicCondition">
<planItemOnPart sourceRef="previousTask">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<condition>${amount > 10000}</condition>
</sentry>
<!-- 复杂条件组合 -->
<sentry id="complexCondition">
<planItemOnPart sourceRef="taskA">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<planItemOnPart sourceRef="taskB">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<condition>
${taskA.output == 'APPROVED' && taskB.output == 'APPROVED'}
</condition>
</sentry>
五、性能优化与实战数据
性能对比数据
| 指标 | BPMN方案 | CMMN方案 | 提升幅度 |
|---|---|---|---|
| 平均处理时间 | 5.2天 | 3.1天 | 40% |
| 流程变更发布时间 | 3周 | 3天 | 86% |
| 异常案件处理效率 | 低 | 高 | 显著提升 |
| 用户满意度 | 6.5/10 | 8.7/10 | 34% |
数据库优化策略
sql
-- CMMN特有的大表索引优化
CREATE INDEX idx_cmmn_ru_planitem_case ON act_cmmn_ru_plan_item_inst(CASE_INST_ID_);
CREATE INDEX idx_cmmn_ru_sentry_case ON act_cmmn_ru_sentry_part_inst(CASE_INST_ID_);
CREATE INDEX idx_cmmn_ru_variable_case ON act_cmmn_ru_variable(CASE_INST_ID_);
-- 历史数据归档策略
CREATE EVENT archive_cmmn_histories
ON SCHEDULE EVERY 1 DAY
DO BEGIN
-- 归档6个月前的已完成案例
INSERT INTO act_cmmn_hi_case_inst_archive
SELECT * FROM act_cmmn_hi_case_inst
WHERE END_TIME_ < DATE_SUB(NOW(), INTERVAL 180 DAY);
DELETE FROM act_cmmn_hi_case_inst
WHERE END_TIME_ < DATE_SUB(NOW(), INTERVAL 180 DAY);
END;
六、踩坑记录与解决方案
坑1:哨兵条件的事务边界
java
// 错误示例:在事务外修改案例变量
@Service
public class ProblematicCaseService {
@Transactional
public void completeTask(String taskId) {
// 完成任务
cmmnTaskService.complete(taskId);
// 在事务外修改变量(危险!)
new Thread(() -> {
// 异步更新可能破坏哨兵条件的一致性
updateExternalData(caseInstanceId);
}).start();
}
}
// 解决方案:使用案例事件监听器
@Component
public class SafeCaseService {
@EventListener
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(CaseEvent event) {
// 事务提交后安全地执行后续操作
updateExternalData(event.getCaseInstanceId());
}
}
坑2:计划项的生命周期管理
java
// 正确管理动态计划项
public class PlanItemLifecycleManager {
public void manageDynamicPlanItem(String caseInstanceId) {
// 1. 创建计划项
PlanItemInstance planItem = cmmnRuntimeService
.createPlanItemInstanceBuilder()
.caseInstanceId(caseInstanceId)
.planItemDefinitionId("dynamicTask")
.add();
// 2. 明确的状态管理
cmmnRuntimeService.startPlanItemInstance(planItem.getId());
// 3. 完成后清理
cmmnRuntimeService.completePlanItemInstance(planItem.getId());
// 避免计划项泄漏和状态不一致
}
}
七、CMMN适用场景评估
适合CMMN的场景
java
// 场景1:知识工作流程
public class KnowledgeWorkflow {
// 保险理赔、医疗诊断、法律案件
// 需要专业人员根据情况决定下一步行动
}
// 场景2:动态适应性流程
public class AdaptiveProcess {
// 客户投诉处理、突发事件响应
// 路径无法预先完全定义
}
// 场景3:多维度协调工作
public class MultiDimensionalCoordination {
// 项目管理系统、产品研发流程
// 多个团队并行工作,需要动态协调
}
不适合CMMN的场景
java
// 场景1:标准化操作流程
public class StandardizedProcess {
// 订单处理、数据ETL流程
// 步骤固定,不需要动态调整
}
// 场景2:简单线性流程
public class SimpleLinearProcess {
// 用户注册、密码重置
// 用CMMN是过度设计
}
八、架构师的经验总结
1. 案例设计原则
java
// 原则1:案例阶段划分
public class CaseStageDesign {
public void designCaseStages() {
// 明确阶段划分:接收→评估→决策→结算
// 每个阶段有明确的入口和出口标准
}
}
// 原则2:任务自治性
public class TaskAutonomy {
// 每个任务应该是自包含的
// 尽量减少任务间的硬性依赖
}
// 原则3:异常处理设计
public class ExceptionHandlingDesign {
// 为每个阶段设计异常处理机制
// 支持案例的"优雅降级"
}
2. 团队协作模式
成功要素:
- 业务分析师:负责案例阶段和哨兵条件设计
- 开发工程师:实现任务处理逻辑和集成点
- 测试工程师:设计基于场景的测试用例
工具支持:
- CMMN模型设计器:可视化案例设计
- 案例监控看板:实时跟踪案例状态
- 哨兵条件验证工具:测试条件逻辑
九、写在最后
这次CMMN的实施让我们深刻理解了案例驱动 与流程驱动 的本质区别。最大的收获不是技术层面的,而是思维模式的转变------从"如何定义流程"到"如何支持知识工作"。
最重要的经验:CMMN不是BPMN的替代品,而是解决另一类问题的工具。选择的关键在于识别业务的本质:是重复性的标准操作,还是需要灵活应对的知识工作。
对于考虑CMMN的团队,我的建议是:先从一个小而具体的案例开始,体验动态任务管理的威力,再逐步推广到更复杂的场景。CMMN的学习曲线比BPMN陡峭,但一旦掌握,它能解决传统工作流无法应对的复杂问题。
技术选型的真谛:不是追求最新最热的技术,而是为具体问题找到最合适的解决方案。CMMN就是我们为非结构化知识工作找到的那个"合适方案"。