订单系统状态机,实现订单状态的合法流转,并确保状态变更的幂等性

一、前言

在电商系统中,订单有着复杂多变的状态流转,从待付款、已付款、已发货到已完成、已取消等。如何高效、准确地管理订单状态流转,是一个关键问题。

二、初步设计

2.1 订单状态枚举定义

首先,定义一个订单状态的枚举类,明确所有可能的订单状态。

public enum OrderStatus {
    UNPAID("待付款"),
    PAID("已付款"),
    SHIPPED("已发货"),
    COMPLETED("已完成"),
    CANCELLED("已取消");

    private final String description;

    OrderStatus(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

2.2 状态转移规则定义

定义一个状态转移规则的枚举类,明确每种状态的合法转移目标。

public enum OrderStatusTransition {
    UNPAID_TO_PAID(OrderStatus.UNPAID, OrderStatus.PAID),
    UNPAID_TO_CANCELLED(OrderStatus.UNPAID, OrderStatus.CANCELLED),
    PAID_TO_SHIPPED(OrderStatus.PAID, OrderStatus.SHIPPED),
    SHIPPED_TO_COMPLETED(OrderStatus.SHIPPED, OrderStatus.COMPLETED),
    PAID_TO_CANCELLED(OrderStatus.PAID, OrderStatus.CANCELLED);

    private final OrderStatus sourceStatus;
    private final OrderStatus targetStatus;

    OrderStatusTransition(OrderStatus sourceStatus, OrderStatus targetStatus) {
        this.sourceStatus = sourceStatus;
        this.targetStatus = targetStatus;
    }

    public OrderStatus getSourceStatus() {
        return sourceStatus;
    }

    public OrderStatus getTargetStatus() {
        return targetStatus;
    }

    // 根据源状态和目标状态查找合法转移规则
    public static boolean isValidTransition(OrderStatus sourceStatus, OrderStatus targetStatus) {
        for (OrderStatusTransition transition : OrderStatusTransition.values()) {
            if (transition.getSourceStatus() == sourceStatus && transition.getTargetStatus() == targetStatus) {
                return true;
            }
        }
        return false;
    }
}

3.3 状态变更逻辑实现

在订单服务类中,实现状态变更的方法,确保只有合法的状态转移才能执行。

@Service
public class OrderService {

    public void changeOrderStatus(Order order, OrderStatus targetStatus) {
        OrderStatus currentStatus = order.getStatus();
        if (!OrderStatusTransition.isValidTransition(currentStatus, targetStatus)) {
            throw new IllegalArgumentException("非法的状态转移:" + currentStatus + " -> " + targetStatus);
        }
        // 执行状态变更操作
        order.setStatus(targetStatus);
        // 其他业务逻辑,如更新数据库、发送通知等
    }
}

三、考虑幂等性

为了确保状态变更操作的幂等性,我们可以在执行状态变更之前,先检查订单的当前状态是否与期望的源状态一致。如果一致,则进行状态变更;如果不一致,则说明该状态变更请求可能是重复的,直接忽略。

@Service
public class OrderService {

    public void changeOrderStatus(Order order, OrderStatus targetStatus) {
        OrderStatus currentStatus = order.getStatus();
        if (!OrderStatusTransition.isValidTransition(currentStatus, targetStatus)) {
            throw new IllegalArgumentException("非法的状态转移:" + currentStatus + " -> " + targetStatus);
        }
        if (currentStatus != targetStatus) {
            // 执行状态变更操作
            order.setStatus(targetStatus);
            // 其他业务逻辑,如更新数据库、发送通知等
        } else {
            // 状态未改变,可能是重复请求,直接返回
            System.out.println("状态未改变,可能是重复请求:" + currentStatus + " -> " + targetStatus);
        }
    }
}

四、业务场景模拟

public class OrderSystemSimulation {

    public static void main(String[] args) {
        OrderService orderService = new OrderService();
        Order order = new Order(OrderStatus.UNPAID);

        // 合法状态转移:待付款 -> 已付款
        orderService.changeOrderStatus(order, OrderStatus.PAID);
        System.out.println("订单状态:" + order.getStatus().getDescription());

        // 合法状态转移:已付款 -> 已发货
        orderService.changeOrderStatus(order, OrderStatus.SHIPPED);
        System.out.println("订单状态:" + order.getStatus().getDescription());

        // 合法状态转移:已发货 -> 已完成
        orderService.changeOrderStatus(order, OrderStatus.COMPLETED);
        System.out.println("订单状态:" + order.getStatus().getDescription());

        // 非法状态转移:已完成 -> 已付款
        try {
            orderService.changeOrderStatus(order, OrderStatus.PAID);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }

        // 重复状态变更请求:已完成 -> 已完成(幂等性测试)
        orderService.changeOrderStatus(order, OrderStatus.COMPLETED);
        System.out.println("订单状态:" + order.getStatus().getDescription());
    }
}

运行结果:

订单状态:已付款
订单状态:已发货
订单状态:已完成
非法的状态转移:COMPLETED -> PAID
状态未改变,可能是重复请求:COMPLETED -> COMPLETED
订单状态:已完成

五、优化与扩展

5.1 添加状态转移日志记录

在状态变更操作中,记录状态转移的日志信息,包括转移前后的状态、转移时间、操作用户等。这有助于后续的问题排查和业务分析。

@Service
public class OrderService {

    public void changeOrderStatus(Order order, OrderStatus targetStatus, String operator) {
        OrderStatus currentStatus = order.getStatus();
        if (!OrderStatusTransition.isValidTransition(currentStatus, targetStatus)) {
            throw new IllegalArgumentException("非法的状态转移:" + currentStatus + " -> " + targetStatus);
        }
        if (currentStatus != targetStatus) {
            // 记录状态转移前的状态
            order.setPreviousStatus(currentStatus);
            // 执行状态变更操作
            order.setStatus(targetStatus);
            // 记录状态转移日志
            OrderStatusLog statusLog = new OrderStatusLog();
            statusLog.setOrderId(order.getId());
            statusLog.setFromStatus(currentStatus);
            statusLog.setToStatus(targetStatus);
            statusLog.setOperator(operator);
            statusLog.setOperationTime(new Date());
            statusLog.save();
            // 其他业务逻辑,如更新数据库、发送通知等
        } else {
             //态未改变,可能是重复请求,记录日志并返回
            System.out.println("状态未改变,可能是重复请求:" + currentStatus + " -> " + targetStatus);
        }
    }
}

5.2 支持状态转移的条件判断某些状态转移可能需要满足特定的条件才能执行。例如,订单从 "已付款" 转移到 "已发货" 状态时,需要库存充足。我们可以在状态转移规则中添加条件判断逻辑。

public enum OrderStatusTransition {
    UNPAID_TO_PAID(OrderStatus.UNPAID, OrderStatus.PAID, null),
    UNPAID_TO_CANCELLED(OrderStatus.UNPAID, OrderStatus.CANCELLED, null),
    PAID_TO_SHIPPED(OrderStatus.PAID, OrderStatus.SHIPPED, "库存充足"),
    SHIPPED_TO_COMPLETED(OrderStatus.SHIPPED, OrderStatus.COMPLETED, null),
    PAID_TO_CANCELLED(OrderStatus.PAID, OrderStatus.CANCELLED, "退款成功");

    private final OrderStatus sourceStatus;
    private final OrderStatus targetStatus;
    private final String conditionDescription;

    OrderStatusTransition(OrderStatus sourceStatus, OrderStatus targetStatus, String conditionDescription) {
        this.sourceStatus = sourceStatus;
        this.targetStatus = targetStatus;
        this.conditionDescription = conditionDescription;
    }

    public OrderStatus getSourceStatus() {
        return sourceStatus;
    }

    public OrderStatus getTargetStatus() {
        return targetStatus;
    }

    public String getConditionDescription() {
        return conditionDescription;
    }

    // 根据源状态和目标状态查找合法转移规则
    public static boolean isValidTransition(OrderStatus sourceStatus, OrderStatus targetStatus) {
        for (OrderStatusTransition transition : OrderStatusTransition.values()) {
            if (transition.getSourceStatus() == sourceStatus && transition.getTargetStatus() == targetStatus) {
                return true;
            }
        }
        return false;
    }

    // 根据源状态和目标状态获取状态转移条件描述
    public static String getConditionDescription(OrderStatus sourceStatus, OrderStatus targetStatus) {
        for (OrderStatusTransition transition : OrderStatusTransition.values()) {
            if (transition.getSourceStatus() == sourceStatus && transition.getTargetStatus() == targetStatus) {
                return transition.getConditionDescription();
            }
        }
        return null;
    }
}
@Service
public class OrderService {

    @Autowired
    private InventoryService inventoryService;

    @Autowired
    private PaymentService paymentService;

    public void changeOrderStatus(Order order, OrderStatus targetStatus, String operator) {
        OrderStatus currentStatus = order.getStatus();
        if (!OrderStatusTransition.isValidTransition(currentStatus, targetStatus)) {
            throw new IllegalArgumentException("非法的状态转移:" + currentStatus + " -> " + targetStatus);
        }

        String conditionDescription = OrderStatusTransition.getConditionDescription(currentStatus, targetStatus);
        if (conditionDescription != null && !checkTransitionCondition(order, conditionDescription)) {
            throw new IllegalStateException("状态转移条件不满足:" + conditionDescription);
        }

        if (currentStatus != targetStatus) {
            // 记录状态转移前的状态
            order.setPreviousStatus(currentStatus);
            // 执行状态变更操作
            order setStatus(targetStatus);
            // 记录状态转移日志
            OrderStatusLog statusLog = new OrderStatusLog();
            statusLog.setOrderId(order.getId());
            statusLog.setFromStatus(currentStatus);
            statusLog.setToStatus(targetStatus);
            statusLog.setOperator(operator);
            statusLog.setOperationTime(new Date());
            statusLog.save();
            // 其他业务逻辑,如更新数据库、发送通知等
        } else {
            // 状态未改变,可能是重复请求,记录日志并返回
            System.out.println("状态未改变,可能是重复请求:" + currentStatus + " -> " + targetStatus);
        }
    }

    private boolean checkTransitionCondition(Order order, String conditionDescription) {
        switch (conditionDescription) {
            case "库存充足":
                return inventoryService.checkInventory(order.getProductId(), order.getQuantity());
            case "退款成功":
                return paymentService.refund(order.getId());
            default:
                return true;
        }
    }
}

5.3 可视化状态机配置

为了提高系统的可维护性和灵活性,可以将状态机的配置存储在外部文件(如 XML、JSON 或数据库)中,实现可视化状态机配置。以 XML 配置文件为例:

<statemachine name="OrderStatusStateMachine">
    <states>
        <state name="UNPAID" description="待付款"/>
        <state name="PAID" description="已付款"/>
        <state name="SHIPPED" description="已发货"/>
        <state name="COMPLETED" description="已完成"/>
        <state name="CANCELLED" description="已取消"/>
    </states>
    <transitions>
        <transition name="UNPAID_TO_PAID" from="UNPAID" to="PAID"/>
        <transition name="UNPAID_TO_CANCELLED" from="UNPAID" to="CANCELLED"/>
        <transition name="PAID_TO_SHIPPED" from="PAID" to="SHIPPED" condition="库存充足"/>
        <transition name="SHIPPED_TO_COMPLETED" from="SHIPPED" to="COMPLETED"/>
        <transition name="PAID_TO_CANCELLED" from="PAID" to="CANCELLED" condition="退款成功"/>
    </transitions>
</statemachine>

然后,通过解析 XML 配置文件来构建状态机的运行时模型,实现状态和转移规则的动态加载与管理。

六、总结

在面对订单管理系统中的状态机设计问题,要充分考虑以下几个关键点:

  • 1、明确所有可能的订单状态及其含义。
  • 2、定义合法的状态转移规则,确保操作只能向合法状态跳转。
  • 3、考虑幂等性,避免重复请求导致的状态混乱。
  • 4、记录状态转移日志,便于后续问题排查和业务分析。
  • 5、支持状态转移的条件判断,满足实际业务需求。
  • 6、采用可视化状态机配置,提高系统的可维护性和灵活性。

原文出处:https://mp.weixin.qq.com/s/hzna02_AJAK6OExpW4hB0g

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容