Yujun's Blog

以状态为线索,重塑业务流程图

July 21, 2025 (1w ago)SD

以状态为线索,重塑业务流程图

什么是枚举? 在编程中,我们经常会遇到一些变量,它的取值范围是有限且固定的。比如:

  • 星期: 只有周一、周二、周三、周四、周五、周六、周日这7个值。
  • 性别: 只有男、女。
  • 订单状态: 只有待支付、已支付、已发货、已完成、已取消。

在没有枚举之前,程序员通常使用魔法数字(Magic Numbers)或字符串常量来表示这些状态:

  • int status = 0; (0代表什么?没人知道,需要查文档)
  • String status = "NEW"; (容易写错,比如写成 "new" 或 "New")

枚举的出现就是为了解决这些问题。它允许我们创建一个新的类型,这个类型的所有合法实例(对象)在定义时就已经全部列举出来了。

枚举充血

在复杂的业务场景中,一个决策往往依赖于多个状态的组合。例如,判断一个用户是否有资格参与某个活动,可能需要同时检查 ActivityStatusEnumVO、TagScopeEnumVO 和 GroupBuyOrderEnumVO。通过为这些枚举设计巧妙的方法,可以将复杂的布尔逻辑封装起来,向上层提供一个简单的业务查询接口,如 activity.isEligibleFor(user)。

普通的常量,就是个死的数据。但Java里的枚举,特别是DDD中的,它是个的,我们可以在里面写方法。

举个例子,我们项目里有个“退款类型”的枚举 RefundTypeEnum,里面有好几种情况,比如“没给钱就想退的”、“给了钱但团还没成的”、“团成了又要退的”。每种情况的处理逻辑(策略)都不一样。

菜鸟的写法,可能会在某个服务类里写一长串的 if-else 或者 switch:

// 这种写法,迟早有一天会变得又臭又长
public void refund(RefundTypeEnum type) {
    if (type == RefundTypeEnum.UNPAID_UNLOCK) {
        // 执行A策略...
    } else if (type == RefundTypeEnum.PAID_UNFORMED) {
        // 执行B策略...
    } else if (type == RefundTypeEnum.PAID_FORMED) {
        // 执行C策略...
    }
    // ...未来可能还有D、E、F...
}

这种代码,每次加一种新的退款类型,都得跑回来改这里,特别烦人,还容易改出新bug。

高手的写法,是让枚举自己动起来:

public enum RefundTypeEnum {
    UNPAID_UNLOCK {
        @Override
        public void doRefund() { System.out.println("执行A策略:没给钱?直接关单!"); }
    },
    PAID_UNFORMED {
        @Override
        public void doRefund() { System.out.println("执行B策略:原路退款,没啥说的。"); }
    },
    PAID_FORMED {
        @Override
        public void doRefund() { System.out.println("执行C策略:哎呀,这个得扣点手续费了..."); }
    };
    
    // 让每个枚举自己说出该怎么做
    public abstract void doRefund();
}

我们把做什么(doRefund)的逻辑,直接放到了是什么(UNPAID_UNLOCK)的定义里。

现在,业务代码就变得超级干净清爽:

// 不管你是什么类型,你自己知道该怎么办,执行就完事。
refundType.doRefund();

以后再加一种新的退款类型?简单,就在枚举文件里加个新成员,实现它的 doRefund 方法就行了。其他地方的代码,一个字都不用改。这就叫“对扩展开放,对修改关闭”。

一句话总结这层:枚举不光能当“名词”,还能当“动词”。它能把相关的判断逻辑和行为都放进去,让业务代码变得干净又优雅。

枚举的顶峰:构建“微型领域模型”

当我们将视野拉高,审视一个项目中所有的枚举类时。这些看似独立的枚举,如 ActivityStatusEnumVO(活动状态)、GroupBuyOrderEnumVO(拼团订单状态)、TradeOrderStatusEnumVO(交易订单状态),共同交织,它们串联在一起,就是给我们整个项目画了一张业务地图,甚至可以说是免费的架构师,构成了一个业务领域的状态机网络

它们是DDD 中的绝佳实践。一个 PurchaseRequestStatusItemEnum 实例,它就是采购领域中一个不可变的、拥有明确业务含义的“值”。

约束业务边界:枚举定义了业务操作的前置条件和后置状态。一个状态转换方法changeStatus(fromStatus, toStatus)可以利用枚举的序数或预定义的转换规则,来约束哪些状态可以迁移到哪些状态,从而在代码层面固化了业务流程的合法性。 简化复杂决策:在复杂的业务场景中,一个决策往往依赖于多个状态的组合。例如,判断一个用户是否有资格参与某个活动,可能需要同时检查 ActivityStatusEnumVOTagScopeEnumVO GroupBuyOrderEnumVO。通过为这些枚举设计巧妙的方法,可以将复杂的布尔逻辑封装起来,向上层提供一个简单的业务查询接口,如 activity.isEligibleFor(user)

实战:从枚举类复原业务

这个拼团业务的核心,围绕着 用户参与一个拼团活动,最终或成团或失败 这条主线展开。我们来将其分解为以下几个关键阶段和场景。

活动准备和创建(后台管理)

首先,由运营人员在后台配置拼团活动。

  • 新建活动
    • 此时,活动状态为 ActivityStatusEnumVO.CREATE (创建)。
  • 配置折扣:为活动配置优惠。
    • 运营可以选择优惠类型:DiscountTypeEnum.BASE (基础优惠,如所有人都减10元) 或 DiscountTypeEnum.TAG (人群标签,如新用户专享)。
  • 设置人群标签:如果选择了 TAG 类型的优惠,就需要进一步配置人群限制。、
    • TagScopeEnumVO.VISIBLE: 设置哪些人群看得见这个活动。
    • TagScopeEnumVO.ENABLE: 设置哪些人群可以参与这个活动。
  • 活动生效 :运营人员在合适的时机将活动上线。
    • 活动状态从 CREATE 扭转为 ActivityStatusEnumVO.EFFECTIVE (生效)。
    • 系统此时开始进入活动有效期计时。

接着,进入C端场景,也是用户与系统交互最频繁的阶段。

用户发起/参与拼团: 一个用户(我们称他为A)在前端页面点击“发起拼团”或“加入已有团“。这个请求会经过如下流程:

首先,系统会经过一系列严格的校验。

  • 活动状态必须是 ActivityStatusEnumVO.EFFECTIVE (生效) (对应 E0101)。
  • 当前时间必须在活动有效期内 (对应 E0102)。
  • 用户是否在黑名单中 (对应 E0105)。
  • 检查用户参与次数是否已达上限 (对应 E0103)。
  • 检查用户是否满足人群标签限制 TagScopeEnumVO.ENABLE (对应 E0007)。
  • 检查库存是否充足 (对应 E0008)。
  • 系统降级/切量开关是否拦截 (对应 E0003, E0004)。

校验通过后,系统会引导用户去支付,创建交易订单。

  • 系统为用户A的这次参与行为,创建一个交易订单。
  • 此时,交易订单状态为 TradeOrderStatusEnumVO.CREATE (初始创建)。

用户支付,用户完成支付。

  • 系统收到支付成功的回调。
  • 交易订单状态从 CREATE 扭转为 TradeOrderStatusEnumVO.COMPLETE (消费完成)。

创建/更新拼团订单: 这是拼团逻辑的核心

  • 系统根据支付成功的交易订单,去创建或更新一个拼团组队实体。
  • 如果是团长(第一个人): 创建一个新的拼团订单,其初始状态为 GroupBuyOrderEnumVO.PROGRESS (拼单中)。
  • 如果是团员: 找到对应的拼团订单,为其增加一个成员。状态依然是 PROGRESS。
  • 检查是否成团: 每当有新成员加入,系统都会检查当前人数是否已达到成团要求。
    • 未达到: 保持 PROGRESS 状态,等待下一个人。
    • 已达到: 恭喜成团!拼团订单状态从 PROGRESS 扭转为 GroupBuyOrderEnumVO.COMPLETE (完成)。(对应 E0006)
@Getter
public enum ResponseCode {

    SUCCESS("0000", "成功"),
    UN_ERROR("0001", "未知失败"),
    ILLEGAL_PARAMETER("0002", "非法参数"),
    INDEX_EXCEPTION("0003", "唯一索引冲突"),
    UPDATE_ZERO("0004", "更新记录为0"),
    HTTP_EXCEPTION("0005", "HTTP接口调用异常"),
    RATE_LIMITER("0006", "接口限流"),

    E0001("E0001", "不存在对应的折扣计算服务"),
    E0002("E0002", "无拼团营销配置"),
    E0003("E0003", "拼团活动降级拦截"),
    E0004("E0004", "拼团活动切量拦截"),
    E0005("E0005", "拼团组队失败,记录更新为0"),
    E0006("E0006", "拼团组队完结,锁单量已达成"),
    E0007("E0007", "拼团人群限定,不可参与"),
    E0008("E0008", "拼团组队失败,缓存库存不足"),

    E0101("E0101", "拼团活动未生效"),
    E0102("E0102", "不在拼团活动有效时间内"),
    E0103("E0103", "当前用户参与此拼团次数已达上限"),
    E0104("E0104", "不存在的外部交易单号或用户已退单"),
    E0105("E0105", "SC渠道黑名单拦截"),
    E0106("E0106", "订单交易时间不在拼团有效时间范围内"),

    ;

    private String code;
    private String info;

注意,此时,业务还没有完成。最后我们还要关注成团/失败后的自动化处理 (后台任务)。

当一个拼团订单的状态发生终态变化(完成或失败)时,会触发一系列后续的自动化流程。

场景A:拼团成功 (GroupBuyOrderEnumVO.COMPLETE)

  • 触发通知: 系统需要通知所有参团用户“拼团成功”。
  • 通知方式: 根据配置,可能会采用 NotifyTypeEnumVO.HTTP (HTTP回调,通知商户发货) 或 NotifyTypeEnumVO.MQ (MQ消息,触发内部其他流程)。
  • 任务状态: 异步的HTTP通知任务本身也有状态,如 NotifyTaskHTTPEnumVO.SUCCESS 或 ERROR。

场景B:拼团失败 (GroupBuyOrderEnumVO.FAIL)

  • 失败原因: 可能是时间到了人数没凑够,也可能是活动被强制中止。
  • 触发退款: 这是最关键的后续动作。系统需要为所有已支付的团员自动发起退款。
  • 退款逻辑的核心: 系统会调用 RefundTypeEnumVO.getRefundStrategy() 方法。
    • 它会根据当前拼团订单状态 (GroupBuyOrderEnumVO.FAIL) 和每个用户的交易订单状态 (TradeOrderStatusEnumVO.COMPLETE),去匹配一个正确的退款策略。
    • 在这个场景下,它会匹配到 RefundTypeEnumVO.PAID_UNFORMED (已支付,未成团) 这个枚举实例。
    • 然后系统获取到其对应的策略 paid2RefundStrategy,并执行退款操作。
  • 退款结果: 退款操作本身也有结果 TradeRefundBehaviorEnum,可能是 SUCCESS (成功), REPEAT (重复退款,被拦截), FAIL (因银行或其他原因失败)。
  • 更新交易订单状态: 退款成功后,用户的交易订单状态会从 COMPLETE 扭转为 TradeOrderStatusEnumVO.CLOSE (用户退单)。

那么还有没有什么场景是我们没有考虑到的?在拼团过程中,用户也可能主动发起退单。

用户发起退单: 用户在前端点击“取消订单”或“退款”。

  • 前置检查: 系统会检查外部交易单号是否存在且未被关闭 (对应 E0104)。
  • 调用退款策略: 同样是调用 RefundTypeEnumVO.getRefundStrategy()。
    • 如果用户还未支付: 匹配到 RefundTypeEnumVO.UNPAID_UNLOCK (未支付,未成团),策略可能是直接关闭交易订单,并释放库存。
    • 如果用户已支付但团未成: 匹配到 RefundTypeEnumVO.PAID_UNFORMED (已支付,未成团),执行退款。
    • 如果用户已支付且团已成: 匹配到 RefundTypeEnumVO.PAID_FORMED (已支付,已成团),这通常是最复杂的,策略可能会根据业务规则(如“成团后XX小时内可退”)来决定是否允许退款,或是否需要扣除手续费。

最后,活动生命周期结束。

  • 当活动时间到达截止日期,无论活动内部还有多少正在进行的团,活动自身的状态都会扭转为 ActivityStatusEnumVO.OVERDUE (过期)。
  • 运营人员也可以手动将活动中止,状态变为 ActivityStatusEnumVO.ABANDONED (废弃)。

通过这套梳理,我们不仅基本了解拼团业务的全貌,更能深刻体会到这些枚举是如何作为业务规则的载体和状态机的基石,将复杂的业务逻辑拆解得井井有条,使得整个系统既健壮又易于扩展。

总结

下一次,当我们再去定义一个Enum时,请不要仅仅将它看作一串静态的 String 或 int 的替代品。尝试去思考:

  • 这个状态或者类型,是否有关联的行为可以被封装进来。
  • 它与其他状态之间,是否存在可以被代码化的转换规则?
  • 它能否成为一个更丰富的“值对象”,即充血对象,帮助我们构建一个更清晰、更健壮的领域模型?

善用枚举,就是善用抽象;精通枚举,方能洞见非凡。 在这小小的 enum 关键字背后,蕴藏着通往高质量、可维护代码的康庄大道。

Comments