如何解构复杂业务

001 流程引擎技术侧收益

一、背景&目标

1.1 背景

  • crm技术侧在对历史代码维护和迭代开发时发现存在很多问题 1、代码逻辑混乱,不同子域业务逻辑没有做隔离,导致迭代开发时熟悉代码成本很高,难以维护,容易出现遗漏,极易出现循环依赖问题。 2、代码流程混乱,初期没有考虑到功能的扩展问题,历史代码几乎没有任何拓展接口。 3、模型充血,大泥球效应严重,一个类提供多重功能,无法精细化控制和热插拔,很多代码有问题但是不敢轻易改动,增加逻辑时易影响到原有的功能。

思考点:提到编码,我们常常说起的一个问题就是:应该采用贫血模型还是充血模型? 1.贫血模型,英文叫做 Anemic Domain Model ,是 Martin Fowler 在 2003 年提出的。贫血模型指的是领域对象中只有数据,没有行为,由于过于单薄,就好像人贫血了一样,显得不太健康。这种风格违背了面向对象的原则。

2.充血模型,至于所谓充血模型则是后人提出的,这个人还把模型分成四种:失血模型、贫血模型、充血模型和胀血模型。

综上:我们的业务对象纠集过多的业务逻辑就会变成胀血模型,也就是屎山的开始。显然现在的bpms符合胀血模型的特征。

1.2 目标

  • 考虑到单体服务,业务杂糅在一起,随着业务复杂度的增加和代码风格的差异,历史代码变得越来越臃肿,必然会出现执行效率低下,线上bug激增,难以定位bug点且修复也是频繁的打补丁。

因此我们选型流程引擎时要考虑到以下要点:

1、满足需求的快速上线,仅占用极少的业务抽离和子域划分时间

2、为了解业务和规范接口将流程分为两种: a: 通用流程(通用流程提供统一能力 如入参校验,数据组装,事件后处理) b: 逻辑流程 (根据业务的不同抽离并分割逻辑,将分割好的逻辑组件化,异步的组件异步执行,串行的组件顺序执行,组件间可通过配置文件编排顺序,可监控执行时间,可回滚,可重试)

3、流程引擎占用极少的系统资源

二、选型

维度LiteFlow(流程编排引擎)Flowable / Camunda / Activiti(BPMN引擎)
📌 核心理念“流程即代码”“流程即模型(BPMN 图)”
🧠 面向人群程序员业务 + 程序员协作
🧱 建模方式Java 配置 / 注解 / XML/数据库规则/applo配置BPMN 图形化(可拖图)
⚙️ 编排方式规则链表达式 THEN, WHEN, IF流程图 + 流程引擎驱动
🏗 灵活性高度灵活、轻量、可嵌套更结构化、更规范
📊 可视化❌ 不支持流程图✅ 内建流程建模器(Modeler)
🔁 支持异步/条件/并发✅ 很强(内建线程池+异步组件)✅ 支持(但偏重同步)
💼 典型用途编排服务调用、接口逻辑、规则判断审批流、业务流转、工作流
🔧 上手成本极低(就是写代码)偏高(要理解 BPMN 语义)
🧩 整合性和 Spring Boot 深度整合/也可支持其他框架多为标准化接口
⛓ 节点动态性✅ 动态增加/替换组件❌ 一般需要重新部署流程

LiteFlow 的主要 好处 总结如下:


2.1轻量、低侵入性

  • 不依赖 Spring、MyBatis 等重框架,可单独使用

  • 没有冗余配置,也不用引入笨重的 BPMN 模型

  • 可快速集成到现有项目中,基本“开箱即用”


2.2 流程规则灵活、可配置

  • 支持 字符串 DSLEL 表达式XML/JSON/YAML 等多种规则定义方式

  • 支持流程的 热更新,可接入配置中心(如 Nacos、Apollo)

  • 无需重启服务就能改流程!对业务团队极友好


2.3 可视化逻辑清晰、组件复用

  • 将流程拆成独立节点(组件),每个组件只关注“做什么”

  • 组件可以复用到多个流程中,类似积木搭建

  • 整个流程就像组合逻辑,用编排就能完成


2.4 支持复杂流程结构

  • 支持 串行、并行、条件判断(IF/ELSE)、SWITCH、多分支、循环、异常处理、异步 等结构

  • 完全覆盖一般企业业务流程复杂度需求


2.5 易于扩展、调试友好

  • 流程组件是标准 Java Bean,易于开发、测试、调试

  • 可结合日志打印、监控系统,定位流程问题非常方便


2.6 内置能力丰富(开箱即用)

  • 异常捕获、上下文管理、并发处理、标签匹配、多语言规则支持

  • 社区活跃,文档齐全,易于维护


总结一句话:

业务即流程,逻辑即组件。

LiteFlow 就是把你脑子里的业务流程变成代码里的“拼图”,流程可配、组件复用,既强大又灵活。

三、解业务Demo

见代码

四、核心理念

4.1 通用流程:代码规范化

花费极少的人力去规范化代码

validate node(只要将以下代码沾到ai中,下次需要生成validate类只需要将被校验的类拷到ai就可自动生成)

public class GetAccessTokenResponseValidator {

    public static void validate(GetAccessTokenResponse response) {
        AssertUtils.assertNotNull(response, "[GetAccessTokenResponse] should not be null");
        AssertUtils.assertNotNull(response.getBody(), "[GetAccessTokenResponse.body] should not be null");
        AssertUtils.assertNotNull(
                response.getBody().getAccessToken(), "[GetAccessTokenResponse.body.accessToken] should not be null");
    }
}

@Data
public class GetAccessTokenResponse extends TeaModel {
    @NameInMap("headers")
    public Map<String, String> headers;
    @NameInMap("statusCode")
    public Integer statusCode;
    @NameInMap("body")
    public GetAccessTokenResponseBody body;
}
package com.tecdo.crm.common.utils;

import java.util.Collection;
import java.util.Set;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;

import com.tecdo.crm.common.exception.ValidationException;

/**
 * @Description: 自定义断言校验工具类
 * @Author: zuojipeng
 * @Date: 2025/1/17 10:14
 * @Version: 1.0
 **/
public class AssertUtils {

    private static final Validator VALIDATOR =
            Validation.buildDefaultValidatorFactory().getValidator();

    public static <T> void validate(T object) {
        if (object == null) {
            throw new IllegalArgumentException("The object to validate cannot be null.");
        }

        Set<ConstraintViolation<T>> violations = VALIDATOR.validate(object);
        if (!violations.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (ConstraintViolation<T> violation : violations) {
                sb.append(violation.getPropertyPath())
                        .append(" ")
                        .append(violation.getMessage())
                        .append("; ");
            }
            throw new ValidationException(sb.toString());
        }
    }

    // 断言条件是否为真
    public static void assertTrue(boolean condition, String message) {
        if (!condition) {
            throw new IllegalArgumentException(message);
        }
    }

    // 断言对象是否为非空
    public static void assertNotNull(Object object, String message) {
        if (object == null) {
            throw new IllegalArgumentException(message);
        }
    }

    // 断言对象是否为空
    public static void assertNull(Object object, String message) {
        if (object != null) {
            throw new IllegalArgumentException(message);
        }
    }

    // 断言字符串是否为空
    public static void assertNotEmpty(String str, String message) {
        if (str == null || str.trim().isEmpty()) {
            throw new IllegalArgumentException(message);
        }
    }

    // 断言集合是否为空
    public static void assertNotEmpty(Collection<?> collection, String message) {
        if (collection == null || collection.isEmpty()) {
            throw new IllegalArgumentException(message);
        }
    }

    // 断言两个值是否相等
    public static void assertEquals(Object expected, Object actual, String message) {
        if (expected == null && actual != null || expected != null && !expected.equals(actual)) {
            throw new IllegalArgumentException(message);
        }
    }

    // 断言条件是否为假
    public static void assertFalse(boolean condition, String message) {
        if (condition) {
            throw new IllegalArgumentException(message);
        }
    }
}

adapter node (只要将以下代码沾到ai中,下次需要生成validate类只需要将被校验的类拷到ai就可自动生成)

package com.tecdo.crm.core.adapter;

import java.util.List;
import java.util.stream.Collectors;

import com.tecdo.crm.core.db.crm.entity.LeadFollowKpi;
import com.tecdo.crm.core.dto.LeadFollowKpiDTO;

/**
 * @Description: 线索kpi
 * @Author: zuojipeng
 * @Date: 2025/1/23 10:43
 * @Version: 1.0
 **/
public class LeadFollowKpiAdapter {

    /**
     * Converts LeadFollowKpiDTO to LeadFollowKpi entity.
     *
     * @param dto the LeadFollowKpiDTO to convert
     * @return the converted LeadFollowKpi entity
     */
    public static LeadFollowKpi adapt(LeadFollowKpiDTO dto) {
        if (dto == null) {
            return null;
        }

        LeadFollowKpi entity = new LeadFollowKpi();
        entity.setStatDate(dto.getStatDate());
        entity.setLeadId(dto.getLeadId());
        entity.setCustomerId(dto.getCustomerId());
        entity.setAdvId(dto.getAdvId());
        entity.setMediaPlatform(dto.getMediaPlatform());
        entity.setAdvName(dto.getAdvName());
        entity.setCooperationType(dto.getCooperationType());
        entity.setBdId(dto.getBdId());
        entity.setBdName(dto.getBdName());
        entity.setBdNameEn(dto.getBdNameEn());
        entity.setPaId(dto.getPaId());
        entity.setPaName(dto.getPaName());
        entity.setLeadCreateDate(dto.getLeadCreateDate());
        entity.setLeadDiscoveryDate(dto.getLeadDiscoveryDate());
        entity.setQuotationId(dto.getQuotationId());
        entity.setCrmServiceItem(dto.getCrmServiceItem());
        entity.setFirstAdvSpendDate(dto.getFirstAdvSpendDate());
        entity.setAdvPlatSpendAmt1d(dto.getAdvPlatSpendAmt1d());
        entity.setAdvPlatMarginAmt1d(dto.getAdvPlatMarginAmt1d());
        entity.setCreateAccCnt1d(dto.getCreateAccCnt1d());
        entity.setNewAccShopCnt1d(dto.getNewAccShopCnt1d());
        return entity;
    }

    /**
     * Converts a list of LeadFollowKpiDTOs to a list of LeadFollowKpi entities.
     *
     * @param dtoList the list of LeadFollowKpiDTOs
     * @return the converted list of LeadFollowKpi entities
     */
    public static List<LeadFollowKpi> adaptListToEntity(List<LeadFollowKpiDTO> dtoList) {
        if (dtoList == null || dtoList.isEmpty()) {
            return List.of();
        }
        return dtoList.stream().map(LeadFollowKpiAdapter::adapt).collect(Collectors.toList());
    }
}

postHandler node

将通知,推送或和主流程无关的node放在异步的后置节点中执行,用规则语句when控制

4.2 业务流程 (核心流程):规则组件化

解业务,详细流程见代码

五、相关文档和资料

1.https://liteflow.cc/pages/5816c5/#%E5%89%8D%E8%A8%80 2.《领域驱动设计 —— 软件核心复杂性应对之道》----Eric Evans

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值