chore: 【ops】状态机实现/状态切换事件发布/保洁监听处理事件

This commit is contained in:
lzh
2026-01-08 15:05:09 +08:00
parent a30a60245d
commit 5e9dc8b104
7 changed files with 1187 additions and 138 deletions

View File

@@ -0,0 +1,71 @@
package com.viewsh.module.ops.core.event;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 工单完成领域事件
* <p>
* 当工单完成时发布此事件
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderCompletedEvent {
/**
* 工单ID
*/
private Long orderId;
/**
* 工单类型
*/
private String orderType;
/**
* 工单编号
*/
private String orderCode;
/**
* 执行人ID
*/
private Long assigneeId;
/**
* 完成时间
*/
private LocalDateTime completedTime;
/**
* 作业时长(秒)
*/
private Integer workDuration;
/**
* 完成备注
*/
private String remark;
/**
* 扩展参数
*/
@Builder.Default
private java.util.Map<String, Object> payload = new java.util.HashMap<>();
public OrderCompletedEvent addPayload(String key, Object value) {
if (this.payload == null) {
this.payload = new java.util.HashMap<>();
}
this.payload.put(key, value);
return this;
}
}

View File

@@ -0,0 +1,77 @@
package com.viewsh.module.ops.core.event;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 工单创建领域事件
* <p>
* 当新工单创建时发布此事件
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderCreatedEvent {
/**
* 工单ID
*/
private Long orderId;
/**
* 工单类型
*/
private String orderType;
/**
* 工单编号
*/
private String orderCode;
/**
* 工单标题
*/
private String title;
/**
* 区域ID
*/
private Long areaId;
/**
* 优先级
*/
private Integer priority;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 创建人ID
*/
private Long creatorId;
/**
* 扩展参数
*/
@Builder.Default
private Map<String, Object> payload = new java.util.HashMap<>();
public OrderCreatedEvent addPayload(String key, Object value) {
if (this.payload == null) {
this.payload = new java.util.HashMap<>();
}
this.payload.put(key, value);
return this;
}
}

View File

@@ -0,0 +1,40 @@
package com.viewsh.module.ops.core.event;
/**
* 工单事件发布器接口
* <p>
* 职责:
* 1. 发布工单状态变更事件
* 2. 发布工单创建事件
* 3. 发布工单完成事件
* <p>
* 设计说明:
* - 使用领域事件模式解耦状态机与业务逻辑
* - 业务方通过 @EventListener 订阅事件
* - 事件异步发布,不阻塞主流程
*
* @author lzh
*/
public interface OrderEventPublisher {
/**
* 发布状态变更事件
*
* @param event 状态变更事件
*/
void publishStateChanged(OrderStateChangedEvent event);
/**
* 发布工单创建事件
*
* @param event 工单创建事件
*/
void publishOrderCreated(OrderCreatedEvent event);
/**
* 发布工单完成事件
*
* @param event 工单完成事件
*/
void publishOrderCompleted(OrderCompletedEvent event);
}

View File

@@ -0,0 +1,71 @@
package com.viewsh.module.ops.core.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
/**
* 工单事件发布器实现
* <p>
* 使用 Spring 的 ApplicationEventPublisher 发布事件
* 业务方通过 @EventListener 订阅事件
* <p>
* 使用示例:
* <pre>
* &#64;Component
* public class CleanOrderEventHandler {
* &#64;EventListener
* public void onStateChanged(OrderStateChangedEvent event) {
* // 处理保洁工单状态变更
* if ("CLEAN".equals(event.getOrderType())) {
* // ...
* }
* }
* }
* </pre>
*
* @author lzh
*/
@Slf4j
@Service
public class OrderEventPublisherImpl implements OrderEventPublisher {
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void publishStateChanged(OrderStateChangedEvent event) {
try {
applicationEventPublisher.publishEvent(event);
log.debug("状态变更事件已发布: orderId={}, {} -> {}",
event.getOrderId(), event.getOldStatus(), event.getNewStatus());
} catch (Exception e) {
// 事件发布失败不应影响主流程
log.error("发布状态变更事件失败: orderId={}", event.getOrderId(), e);
}
}
@Override
public void publishOrderCreated(OrderCreatedEvent event) {
try {
applicationEventPublisher.publishEvent(event);
log.debug("工单创建事件已发布: orderId={}, orderCode={}",
event.getOrderId(), event.getOrderCode());
} catch (Exception e) {
log.error("发布工单创建事件失败: orderId={}", event.getOrderId(), e);
}
}
@Override
public void publishOrderCompleted(OrderCompletedEvent event) {
try {
applicationEventPublisher.publishEvent(event);
log.debug("工单完成事件已发布: orderId={}, assigneeId={}",
event.getOrderId(), event.getAssigneeId());
} catch (Exception e) {
log.error("发布工单完成事件失败: orderId={}", event.getOrderId(), e);
}
}
}

View File

@@ -0,0 +1,172 @@
package com.viewsh.module.ops.core.event;
import com.viewsh.module.ops.enums.OperatorTypeEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 工单状态变更领域事件
* <p>
* 当工单状态发生变更时发布此事件,业务方订阅此事件处理自己的逻辑
* <p>
* 设计说明:
* - 使用领域事件模式解耦状态机与业务逻辑
* - 业务方通过 @EventListener 订阅事件
* - 支持扩展参数传递额外信息
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderStateChangedEvent {
/**
* 工单ID
*/
private Long orderId;
/**
* 工单类型CLEAN、REPAIR、SECURITY等
*/
private String orderType;
/**
* 工单编号
*/
private String orderCode;
/**
* 旧状态
*/
private WorkOrderStatusEnum oldStatus;
/**
* 新状态
*/
private WorkOrderStatusEnum newStatus;
/**
* 操作人类型
*/
private OperatorTypeEnum operatorType;
/**
* 操作人ID
*/
private Long operatorId;
/**
* 操作时间
*/
private LocalDateTime eventTime;
/**
* 备注/说明
*/
private String remark;
/**
* 扩展参数(可选)
* 用于传递额外的业务信息
* 例如interruptReason, urgentOrderId
*/
@Builder.Default
private Map<String, Object> payload = new java.util.HashMap<>();
/**
* 添加扩展参数
*/
public OrderStateChangedEvent addPayload(String key, Object value) {
if (this.payload == null) {
this.payload = new java.util.HashMap<>();
}
this.payload.put(key, value);
return this;
}
/**
* 获取扩展参数
*/
@SuppressWarnings("unchecked")
public <T> T getPayload(String key, Class<T> type) {
if (this.payload == null) {
return null;
}
Object value = this.payload.get(key);
if (value == null) {
return null;
}
if (type.isInstance(value)) {
return (T) value;
}
return null;
}
/**
* 获取扩展参数(字符串)
*/
public String getPayloadString(String key) {
Object value = getPayload(key, Object.class);
return value != null ? value.toString() : null;
}
/**
* 获取扩展参数Long
*/
public Long getPayloadLong(String key) {
Object value = getPayload(key, Object.class);
if (value == null) {
return null;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Integer) {
return ((Integer) value).longValue();
}
if (value instanceof String) {
try {
return Long.parseLong((String) value);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/**
* 是否包含指定的扩展参数
*/
public boolean hasPayload(String key) {
return this.payload != null && this.payload.containsKey(key);
}
/**
* 静态工厂方法:创建事件
*/
public static OrderStateChangedEvent of(Long orderId, String orderType,
WorkOrderStatusEnum oldStatus,
WorkOrderStatusEnum newStatus,
OperatorTypeEnum operatorType,
Long operatorId,
String remark) {
return OrderStateChangedEvent.builder()
.orderId(orderId)
.orderType(orderType)
.oldStatus(oldStatus)
.newStatus(newStatus)
.operatorType(operatorType)
.operatorId(operatorId)
.eventTime(LocalDateTime.now())
.remark(remark)
.build();
}
}

View File

@@ -1,26 +1,37 @@
package com.viewsh.module.ops.service.fsm;
import com.viewsh.module.ops.core.event.OrderEventPublisher;
import com.viewsh.module.ops.core.event.OrderStateChangedEvent;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.OperatorTypeEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.service.event.OpsOrderEventService;
import com.viewsh.module.ops.service.fsm.event.OrderStateChangedEvent;
import com.viewsh.module.ops.service.fsm.listener.OrderStateChangeListener;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
/**
* 工单状态机核心
* <p>
* 职责:
* 1. 管理工单状态转换规则
* 2. 执行状态转换并记录事件
* 3. 发状态变更监听器
* 3. 发状态变更事件(通过事件发布器)
* <p>
* 设计原则:
* - 单一职责:只负责状态转换规则验证和状态管理
* - 不触发业务逻辑:业务逻辑通过事件订阅处理
* - 事件驱动:状态变更后发布事件,业务方订阅处理
* <p>
* 变更说明:
* - 移除了监听器机制(改为事件发布)
* - 状态机只负责状态转换验证和状态更新
* - 业务逻辑通过 @EventListener 订阅事件处理
*
* @author lzh
*/
@@ -34,6 +45,12 @@ public class OrderStateMachine {
@Resource
private OpsOrderEventService eventService;
/**
* 事件发布器(用于发布状态变更事件)
*/
@Resource
private OrderEventPublisher eventPublisher;
/**
* 状态转换规则(清晰可见)
* Key: 当前状态
@@ -43,72 +60,63 @@ public class OrderStateMachine {
* 保洁流程PENDING → QUEUED → DISPATCHED → CONFIRMED → ARRIVED → COMPLETED
*/
private static final Map<WorkOrderStatusEnum, Set<WorkOrderStatusEnum>> TRANSITIONS = Map.of(
// 初始状态
WorkOrderStatusEnum.PENDING, Set.of(
WorkOrderStatusEnum.QUEUED, // 保洁业务:入队
WorkOrderStatusEnum.DISPATCHED, // 通用业务:直接派单
WorkOrderStatusEnum.CANCELLED
),
// 初始状态
WorkOrderStatusEnum.PENDING, Set.of(
WorkOrderStatusEnum.QUEUED, // 保洁业务:入队
WorkOrderStatusEnum.DISPATCHED, // 通用业务:直接派单
WorkOrderStatusEnum.CANCELLED
),
// 保洁业务特有状态
WorkOrderStatusEnum.QUEUED, Set.of(
WorkOrderStatusEnum.DISPATCHED, // 推送到工牌
WorkOrderStatusEnum.CANCELLED
),
// 保洁业务特有状态
WorkOrderStatusEnum.QUEUED, Set.of(
WorkOrderStatusEnum.DISPATCHED, // 推送到工牌
WorkOrderStatusEnum.CANCELLED
),
WorkOrderStatusEnum.DISPATCHED, Set.of(
WorkOrderStatusEnum.CONFIRMED, // 保洁员确认
WorkOrderStatusEnum.ARRIVED, // 通用业务:直接到岗
WorkOrderStatusEnum.CANCELLED
),
WorkOrderStatusEnum.DISPATCHED, Set.of(
WorkOrderStatusEnum.CONFIRMED, // 保洁员确认
WorkOrderStatusEnum.ARRIVED, // 通用业务:直接到岗
WorkOrderStatusEnum.CANCELLED
),
WorkOrderStatusEnum.CONFIRMED, Set.of(
WorkOrderStatusEnum.ARRIVED, // 感知信标开始作业
WorkOrderStatusEnum.CANCELLED
),
WorkOrderStatusEnum.CONFIRMED, Set.of(
WorkOrderStatusEnum.ARRIVED, // 感知信标开始作业
WorkOrderStatusEnum.CANCELLED
),
// 作业中
WorkOrderStatusEnum.ARRIVED, Set.of(
WorkOrderStatusEnum.PAUSED, // 暂停作业
WorkOrderStatusEnum.COMPLETED // 完成作业
),
// 作业中
WorkOrderStatusEnum.ARRIVED, Set.of(
WorkOrderStatusEnum.PAUSED, // 暂停作业
WorkOrderStatusEnum.COMPLETED // 完成作业
),
// 暂停状态
WorkOrderStatusEnum.PAUSED, Set.of(
WorkOrderStatusEnum.ARRIVED, // 恢复作业
WorkOrderStatusEnum.CANCELLED
),
// 暂停状态
WorkOrderStatusEnum.PAUSED, Set.of(
WorkOrderStatusEnum.ARRIVED, // 恢复作业
WorkOrderStatusEnum.CANCELLED
),
// 终态
WorkOrderStatusEnum.COMPLETED, Collections.emptySet(),
WorkOrderStatusEnum.CANCELLED, Collections.emptySet()
// 终态
WorkOrderStatusEnum.COMPLETED, Collections.emptySet(),
WorkOrderStatusEnum.CANCELLED, Collections.emptySet()
);
/**
* 状态变更监听器列表(支持扩展)
*/
private final List<OrderStateChangeListener> listeners = new ArrayList<>();
/**
* 注册监听器
*
* @param listener 监听器实例
*/
public void registerListener(OrderStateChangeListener listener) {
if (listener != null) {
listeners.add(listener);
log.info("注册状态监听器: {}", listener.getClass().getSimpleName());
}
}
/**
* 执行状态转换(核心方法)
* <p>
* 此方法负责:
* 1. 验证状态转换合法性
* 2. 更新工单状态和相关字段
* 3. 记录事件流
* 4. 发布状态变更事件
*
* @param order 工单对象
* @param newStatus 目标状态
* @param order 工单对象
* @param newStatus 目标状态
* @param operatorType 操作人类型
* @param operatorId 操作人ID
* @param remark 说明
* @param operatorId 操作人ID
* @param remark 说明
* @throws IllegalArgumentException 参数不合法
* @throws IllegalStateException 状态转换不合法
*/
@Transactional(rollbackFor = Exception.class)
public void transition(OpsOrderDO order,
@@ -137,32 +145,57 @@ public class OrderStateMachine {
// 4. 记录事件流
eventService.recordEvent(
order.getId(),
oldStatus.name(),
newStatus.name(),
determineEventType(newStatus),
operatorType.getType(),
operatorId,
remark
order.getId(),
oldStatus.name(),
newStatus.name(),
determineEventType(newStatus),
operatorType.getType(),
operatorId,
remark
);
// 5. 触发监听器(扩展点
notifyListeners(order, oldStatus, newStatus, operatorType, operatorId, remark);
// 5. 发布状态变更事件(替代监听器机制
publishStateChangedEvent(order, oldStatus, newStatus, operatorType, operatorId, remark);
log.info("工单状态转换成功: orderId={}, {} -> {}, operatorType={}, operatorId={}",
order.getId(), oldStatus, newStatus, operatorType, operatorId);
order.getId(), oldStatus, newStatus, operatorType, operatorId);
}
/**
* 检查状态转换是否合法(不执行转换)
*
* @param fromStatus 当前状态
* @param toStatus 目标状态
* @return 是否可以转换
*/
public boolean canTransition(WorkOrderStatusEnum fromStatus, WorkOrderStatusEnum toStatus) {
if (fromStatus == null || toStatus == null) {
return false;
}
Set<WorkOrderStatusEnum> allowedTargets = TRANSITIONS.get(fromStatus);
return allowedTargets != null && allowedTargets.contains(toStatus);
}
/**
* 获取当前状态允许转换到的目标状态集合
*
* @param currentStatus 当前状态
* @return 允许的目标状态集合
*/
public Set<WorkOrderStatusEnum> getAllowedTransitions(WorkOrderStatusEnum currentStatus) {
return TRANSITIONS.getOrDefault(currentStatus, Collections.emptySet());
}
// ==================== 私有方法 ====================
/**
* 校验状态转换是否合法
*/
private void validateTransition(WorkOrderStatusEnum currentStatus, WorkOrderStatusEnum newStatus) {
Set<WorkOrderStatusEnum> allowedTargets = TRANSITIONS.get(currentStatus);
if (allowedTargets == null || !allowedTargets.contains(newStatus)) {
if (!canTransition(currentStatus, newStatus)) {
throw new IllegalStateException(String.format(
"非法状态转换: %s -> %s该转换不被允许",
currentStatus.name(), newStatus.name()
"非法状态转换: %s -> %s该转换不被允许",
currentStatus.name(), newStatus.name()
));
}
}
@@ -172,39 +205,33 @@ public class OrderStateMachine {
*/
private void updateStatusFields(OpsOrderDO order, WorkOrderStatusEnum newStatus) {
switch (newStatus) {
case QUEUED:
case QUEUED -> {
// 入队时无需特殊处理
break;
case DISPATCHED:
}
case DISPATCHED -> {
// 派单时记录派单时间(如有需要)
break;
case CONFIRMED:
// 确认时,保洁员状态由 CleanOrderService 处理
break;
case ARRIVED:
}
case CONFIRMED -> {
// 确认时,保洁员状态由业务层处理
}
case ARRIVED -> {
// 到岗时记录开始时间
order.setStartTime(LocalDateTime.now());
break;
case PAUSED:
// 暂停时,记录暂停时间由保洁业务线处理
break;
case COMPLETED:
}
case PAUSED -> {
// 暂停时,记录暂停时间由业务层处理
}
case COMPLETED -> {
// 完成时记录结束时间
order.setEndTime(LocalDateTime.now());
break;
case CANCELLED:
}
case CANCELLED -> {
// 取消时记录结束时间
order.setEndTime(LocalDateTime.now());
break;
default:
break;
}
default -> {
// 其他状态无需处理
}
}
}
@@ -225,47 +252,35 @@ public class OrderStateMachine {
}
/**
* 触发所有监听器
* 发布状态变更事件
* <p>
* 替代原有的监听器机制:
* - 业务方通过 @EventListener 订阅事件
* - 事件异步发布,不阻塞主流程
*/
private void notifyListeners(OpsOrderDO order,
WorkOrderStatusEnum oldStatus,
WorkOrderStatusEnum newStatus,
OperatorTypeEnum operatorType,
Long operatorId,
String remark) {
if (listeners.isEmpty()) {
return;
private void publishStateChangedEvent(OpsOrderDO order,
WorkOrderStatusEnum oldStatus,
WorkOrderStatusEnum newStatus,
OperatorTypeEnum operatorType,
Long operatorId,
String remark) {
try {
OrderStateChangedEvent event = OrderStateChangedEvent.builder()
.orderId(order.getId())
.orderType(order.getOrderType())
.orderCode(order.getOrderCode())
.oldStatus(oldStatus)
.newStatus(newStatus)
.operatorType(operatorType)
.operatorId(operatorId)
.eventTime(LocalDateTime.now())
.remark(remark)
.build();
eventPublisher.publishStateChanged(event);
} catch (Exception e) {
// 事件发布失败不应影响主流程
log.error("发布状态变更事件失败: orderId={}", order.getId(), e);
}
OrderStateChangedEvent event = OrderStateChangedEvent.builder()
.order(order)
.oldStatus(oldStatus)
.newStatus(newStatus)
.operatorType(operatorType)
.operatorId(operatorId)
.eventTime(LocalDateTime.now())
.remark(remark)
.build();
listeners.forEach(listener -> {
try {
listener.onStateChanged(event);
} catch (Exception e) {
// 监听器异常不影响主流程
log.error("[状态监听器] 执行失败: listener={}, orderId={}, error={}",
listener.getClass().getSimpleName(), order.getId(), e.getMessage(), e);
}
});
}
/**
* 获取当前状态允许转换到的目标状态集合
*
* @param currentStatus 当前状态
* @return 允许的目标状态集合
*/
public Set<WorkOrderStatusEnum> getAllowedTransitions(WorkOrderStatusEnum currentStatus) {
return TRANSITIONS.getOrDefault(currentStatus, Collections.emptySet());
}
}