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,603 @@
package com.viewsh.module.ops.environment.handler;
import cn.hutool.core.map.MapUtil;
import com.viewsh.module.ops.core.event.OrderCompletedEvent;
import com.viewsh.module.ops.core.event.OrderCreatedEvent;
import com.viewsh.module.ops.core.event.OrderStateChangedEvent;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderCleanExtDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderCleanExtMapper;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.environment.constants.CleanNotificationConstants;
import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService;
import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService;
import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastDeduplicationService;
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO;
import com.viewsh.module.system.api.notify.NotifyMessageSendApi;
import com.viewsh.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 保洁工单事件处理器
* <p>
* 职责:
* 1. 订阅工单状态变更事件,处理保洁业务特定逻辑
* 2. 记录保洁扩展信息(到岗时间、完成时间、暂停时间等)
* 3. 更新保洁员状态
* 4. 发送通知语音播报、站内信、IoT震动
* <p>
* 设计说明:
* - 使用 @EventListener 订阅领域事件
* - 业务逻辑同步执行
* - 通知逻辑异步执行(@Async
* - 消息内容使用 {@link CleanNotificationConstants} 统一管理
* <p>
* - 未来迁移 RocketMQ只需修改 @EventListener 为 @RocketMQMessageListener
*
* @author lzh
*/
@Slf4j
@Component
public class CleanOrderEventHandler {
@Resource
private CleanerStatusService cleanerStatusService;
@Resource
private OpsOrderCleanExtMapper cleanExtMapper;
@Resource
private CleanOrderService cleanOrderService;
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private VoiceBroadcastDeduplicationService voiceBroadcastDeduplicationService;
/**
* 站内信发送 APIFeign 客户端,微服务架构下调用 system 服务)
*/
@Resource
private NotifyMessageSendApi notifyMessageSendApi;
/**
* IoT 设备控制 APIFeign 客户端,微服务架构下调用 IoT 服务)
*/
@Resource
private IotDeviceControlApi iotDeviceControlApi;
/**
* 订阅状态变更事件
* <p>
* 只处理保洁类型的工单orderType = "CLEAN"
*/
@EventListener
public void onStateChanged(OrderStateChangedEvent event) {
// 只处理保洁类型的工单
if (!"CLEAN".equals(event.getOrderType())) {
return;
}
log.info("保洁工单状态变更: orderId={}, {} -> {}, operatorId={}",
event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId());
switch (event.getNewStatus()) {
case DISPATCHED:
handleDispatched(event);
break;
case CONFIRMED:
handleConfirmed(event);
break;
case ARRIVED:
handleArrived(event);
break;
case PAUSED:
handlePaused(event);
break;
case COMPLETED:
handleCompleted(event);
break;
case CANCELLED:
handleCancelled(event);
break;
default:
break;
}
}
/**
* 订阅工单创建事件
*/
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
if (!"CLEAN".equals(event.getOrderType())) {
return;
}
log.info("保洁工单已创建: orderId={}, orderCode={}, priority={}",
event.getOrderId(), event.getOrderCode(), event.getPriority());
// 可以在这里触发自动派单逻辑
// 但通常派单逻辑在工单创建时直接调用
}
/**
* 订阅工单完成事件
*/
@EventListener
public void onOrderCompleted(OrderCompletedEvent event) {
if (!"CLEAN".equals(event.getOrderType())) {
return;
}
log.info("保洁工单已完成: orderId={}, assigneeId={}, workDuration={}秒",
event.getOrderId(), event.getAssigneeId(), event.getWorkDuration());
// 自动推送下一个任务
if (event.getAssigneeId() != null) {
cleanOrderService.autoDispatchNextOrder(event.getOrderId(), event.getAssigneeId());
}
// 发送完成通知(异步)
sendOrderCompletedNotification(event.getOrderId());
}
// ==================== 状态处理方法 ====================
/**
* 处理已推送状态(工单已推送到工牌)
*/
private void handleDispatched(OrderStateChangedEvent event) {
Long orderId = event.getOrderId();
Long cleanerId = event.getOperatorId();
// 发送新工单通知(语音+震动+站内信)
sendNewOrderNotification(cleanerId, orderId);
}
/**
* 处理已确认状态(保洁员按下确认按钮)
*/
private void handleConfirmed(OrderStateChangedEvent event) {
Long cleanerId = event.getOperatorId();
// 更新保洁员状态为忙碌
cleanerStatusService.updateStatus(cleanerId,
com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY,
"确认工单");
log.info("保洁员已确认工单状态更新为BUSY: cleanerId={}", cleanerId);
}
/**
* 处理到岗事件
*/
private void handleArrived(OrderStateChangedEvent event) {
Long orderId = event.getOrderId();
// 1. 记录到岗时间到保洁扩展表
OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO();
updateObj.setOpsOrderId(orderId);
updateObj.setArrivedTime(LocalDateTime.now());
cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj);
log.info("保洁员已到岗: orderId={}", orderId);
}
/**
* 处理暂停事件
*/
private void handlePaused(OrderStateChangedEvent event) {
Long orderId = event.getOrderId();
Long operatorId = event.getOperatorId();
// 检查是否是被P0任务打断
String interruptReason = event.getPayloadString("interruptReason");
if ("P0_TASK_INTERRUPT".equals(interruptReason)) {
Long urgentOrderId = event.getPayloadLong("urgentOrderId");
log.warn("保洁任务被P0任务打断: orderId={}, urgentOrderId={}", orderId, urgentOrderId);
// 释放保洁员资源
cleanerStatusService.clearCurrentWorkOrder(operatorId);
// 记录暂停开始时间
recordPauseStartTime(orderId);
} else {
// 普通暂停
log.info("保洁任务已暂停: orderId={}, operatorId={}", orderId, operatorId);
recordPauseStartTime(orderId);
}
}
/**
* 处理完成事件
*/
private void handleCompleted(OrderStateChangedEvent event) {
Long orderId = event.getOrderId();
// 计算作业时长
Integer actualDuration = cleanOrderService.calculateActualDuration(orderId);
log.info("保洁作业完成: orderId={}, actualDuration={}秒", orderId, actualDuration);
// 记录完成时间和时长到保洁扩展表
OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO();
updateObj.setOpsOrderId(orderId);
updateObj.setCompletedTime(LocalDateTime.now());
cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj);
// 保洁员状态恢复(由 autoDispatchNextOrder 处理)
}
/**
* 处理取消事件
*/
private void handleCancelled(OrderStateChangedEvent event) {
Long orderId = event.getOrderId();
Long operatorId = event.getOperatorId();
log.info("保洁工单已取消: orderId={}, operatorId={}", orderId, operatorId);
// 清理保洁员当前工单
if (operatorId != null) {
cleanerStatusService.clearCurrentWorkOrder(operatorId);
}
}
// ==================== 通知方法(异步)====================
/**
* 发送新工单通知(语音播报 + 震动提醒 + 站内信)
*
* @param cleanerId 保洁员ID
* @param orderId 工单ID
*/
@Async("ops-task-executor")
public void sendNewOrderNotification(Long cleanerId, Long orderId) {
try {
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
log.warn("[新工单通知] 工单不存在: orderId={}", orderId);
return;
}
log.info("[新工单通知] cleanerId={}, orderId={}", cleanerId, orderId);
// 1. 语音播报:使用常量
playVoice(cleanerId, CleanNotificationConstants.VoiceMessage.NEW_ORDER);
// 2. 震动提醒:使用常量
vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL);
// 3. 发送站内信
sendNotifyMessageToMember(cleanerId,
CleanNotificationConstants.TemplateCode.NEW_ORDER,
CleanNotificationConstants.NotifyParamsBuilder.newOrderParams(
order.getOrderCode(),
order.getTitle(),
getAreaName(order.getAreaId())
));
} catch (Exception e) {
log.error("[新工单通知] 发送失败: cleanerId={}, orderId={}", cleanerId, orderId, e);
}
}
/**
* 发送待办增加通知(支持去重合并)
*
* @param cleanerId 保洁员ID
* @param queueCount 当前待办数量
*/
@Async("ops-task-executor")
public void sendQueuedOrderNotification(Long cleanerId, int queueCount) {
try {
log.info("[待办增加通知] cleanerId={}, queueCount={}", cleanerId, queueCount);
// 1. 使用去重服务合并播报
voiceBroadcastDeduplicationService.recordAndBroadcast(cleanerId, 1, false);
// 2. 震动提醒(轻量震动):使用常量
vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.LIGHT);
// 3. 发送站内信(可选,避免频繁打扰)
// 如果待办数量较多时才发送站内信
if (queueCount >= 3) {
sendNotifyMessageToMember(cleanerId,
CleanNotificationConstants.TemplateCode.QUEUED_ORDER,
CleanNotificationConstants.NotifyParamsBuilder.queuedOrderParams(queueCount, 1));
}
} catch (Exception e) {
log.error("[待办增加通知] 发送失败: cleanerId={}", cleanerId, e);
}
}
/**
* 发送下一个任务通知
*
* @param cleanerId 保洁员ID
* @param queueCount 待办数量
* @param orderTitle 任务标题
*/
@Async("ops-task-executor")
public void sendNextTaskNotification(Long cleanerId, int queueCount, String orderTitle) {
try {
log.info("[下一任务通知] cleanerId={}, queueCount={}, title={}", cleanerId, queueCount, orderTitle);
// 1. 语音播报:使用常量类格式化
String voiceMessage = CleanNotificationConstants.VoiceHelper.formatNextTask(queueCount, orderTitle);
playVoice(cleanerId, voiceMessage);
// 2. 震动提醒:使用常量
vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL);
// 3. 发送站内信
sendNotifyMessageToMember(cleanerId,
CleanNotificationConstants.TemplateCode.NEXT_TASK,
CleanNotificationConstants.NotifyParamsBuilder.nextTaskParams(queueCount, orderTitle));
} catch (Exception e) {
log.error("[下一任务通知] 发送失败: cleanerId={}", cleanerId, e);
}
}
/**
* 发送P0紧急任务插队通知
*
* @param cleanerId 保洁员ID
* @param orderCode 工单编号
*/
@Async("ops-task-executor")
public void sendPriorityUpgradeNotification(Long cleanerId, String orderCode) {
try {
log.warn("[P0紧急通知] cleanerId={}, orderCode={}", cleanerId, orderCode);
// 1. 语音播报:使用常量类格式化
String voiceMessage = CleanNotificationConstants.VoiceHelper.formatPriorityUpgrade(orderCode);
playVoice(cleanerId, voiceMessage);
// 2. 强烈震动提醒:使用常量
vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.STRONG);
// 3. 发送站内信(高优先级)
sendNotifyMessageToMember(cleanerId,
CleanNotificationConstants.TemplateCode.PRIORITY_UPGRADE,
CleanNotificationConstants.NotifyParamsBuilder.priorityUpgradeParams(orderCode, "P0紧急任务"));
} catch (Exception e) {
log.error("[P0紧急通知] 发送失败: cleanerId={}", cleanerId, e);
}
}
/**
* 发送任务恢复通知
*
* @param cleanerId 保洁员ID
* @param areaName 区域名称
*/
@Async("ops-task-executor")
public void sendTaskResumedNotification(Long cleanerId, String areaName) {
try {
log.info("[任务恢复通知] cleanerId={}, areaName={}", cleanerId, areaName);
// 1. 语音播报:使用常量类格式化
String voiceMessage = CleanNotificationConstants.VoiceHelper.formatTaskResumed(areaName);
playVoice(cleanerId, voiceMessage);
// 2. 震动提醒:使用常量
vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL);
// 3. 发送站内信
sendNotifyMessageToMember(cleanerId,
CleanNotificationConstants.TemplateCode.TASK_RESUMED,
CleanNotificationConstants.NotifyParamsBuilder.taskResumedParams(areaName));
} catch (Exception e) {
log.error("[任务恢复通知] 发送失败: cleanerId={}", cleanerId, e);
}
}
/**
* 发送工单完成通知(通知巡检员验收)
*
* @param orderId 工单ID
*/
@Async("ops-task-executor")
public void sendOrderCompletedNotification(Long orderId) {
try {
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
return;
}
log.info("[工单完成通知] orderId={}, orderCode={}, areaId={}",
orderId, order.getOrderCode(), order.getAreaId());
// TODO: 查询该区域的巡检员列表
// List<Long> inspectorIds = inspectorService.listInspectorsByArea(order.getAreaId());
// for (Long inspectorId : inspectorIds) {
// // 发送站内信给巡检员
// sendNotifyMessageToAdmin(inspectorId,
// CleanNotificationConstants.TemplateCode.ORDER_COMPLETED,
// CleanNotificationConstants.NotifyParamsBuilder.orderCompletedParams(
// order.getOrderCode(), getAreaName(order.getAreaId()), order.getTitle()));
// }
} catch (Exception e) {
log.error("[工单完成通知] 发送失败: orderId={}", orderId, e);
}
}
// ==================== IoT 设备操作方法 ====================
/**
* 语音播报
* <p>
* 通过 RPC 调用 IoT 服务的设备控制接口
*
* @param cleanerId 保洁员ID
* @param message 播报内容
*/
private void playVoice(Long cleanerId, String message) {
try {
// 获取保洁员关联的工牌设备ID
Long deviceId = getBadgeDeviceId(cleanerId);
if (deviceId == null) {
log.warn("[语音播报] 保洁员无关联工牌设备: cleanerId={}", cleanerId);
return;
}
// 构建服务调用请求
IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO();
reqDTO.setDeviceId(deviceId);
reqDTO.setIdentifier("playVoice");
reqDTO.setParams(MapUtil.<String, Object>builder()
.put("text", message)
.put("volume", 80)
.build());
// 调用 IoT 服务
iotDeviceControlApi.invokeService(reqDTO);
log.info("[语音播报] 调用成功: cleanerId={}, deviceId={}, message={}", cleanerId, deviceId, message);
} catch (Exception e) {
log.error("[语音播报] 调用失败: cleanerId={}, message={}", cleanerId, message, e);
}
}
/**
* 震动提醒
* <p>
* 通过 RPC 调用 IoT 服务的设备控制接口
*
* @param cleanerId 保洁员ID
* @param durationMs 震动时长(毫秒)
*/
private void vibrate(Long cleanerId, int durationMs) {
try {
// 获取保洁员关联的工牌设备ID
Long deviceId = getBadgeDeviceId(cleanerId);
if (deviceId == null) {
log.warn("[震动提醒] 保洁员无关联工牌设备: cleanerId={}", cleanerId);
return;
}
// 构建服务调用请求
IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO();
reqDTO.setDeviceId(deviceId);
reqDTO.setIdentifier("vibrate");
reqDTO.setParams(MapUtil.<String, Object>builder()
.put("duration", durationMs)
.put("intensity", 50)
.build());
// 调用 IoT 服务
iotDeviceControlApi.invokeService(reqDTO);
log.info("[震动提醒] 调用成功: cleanerId={}, deviceId={}, durationMs={}", cleanerId, deviceId, durationMs);
} catch (Exception e) {
log.error("[震动提醒] 调用失败: cleanerId={}, durationMs={}", cleanerId, durationMs, e);
}
}
/**
* 获取保洁员关联的工牌设备ID
* <p>
* TODO: 需要根据实际业务逻辑实现
* - 方案1在保洁员表维护关联的设备ID
* - 方案2通过用户ID查询设备表设备表有userId字段
* - 方案3通过中间表维护保洁员与设备的关系
*
* @param cleanerId 保洁员ID
* @return 工牌设备ID无关联返回null
*/
private Long getBadgeDeviceId(Long cleanerId) {
// TODO: 实现获取保洁员关联的工牌设备ID
// 当前返回null实际使用时需要实现具体逻辑
// 示例:
// OpsCleanerDeviceDO device = cleanerDeviceMapper.selectByCleanerId(cleanerId);
// return device != null ? device.getDeviceId() : null;
log.debug("[getBadgeDeviceId] 查询保洁员设备: cleanerId={}", cleanerId);
return null;
}
// ==================== 站内信发送方法 ====================
/**
* 发送站内信给 Member 用户(保洁员)
*
* @param userId 用户ID
* @param templateCode 模板代码
* @param templateParams 模板参数
*/
private void sendNotifyMessageToMember(Long userId, String templateCode,
java.util.Map<String, Object> templateParams) {
try {
NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO();
reqDTO.setUserId(userId);
reqDTO.setTemplateCode(templateCode);
reqDTO.setTemplateParams(templateParams);
notifyMessageSendApi.sendSingleMessageToMember(reqDTO);
log.info("[站内信发送成功] userId={}, templateCode={}", userId, templateCode);
} catch (Exception e) {
log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e);
}
}
/**
* 发送站内信给 Admin 用户(巡检员)
*
* @param userId 用户ID
* @param templateCode 模板代码
* @param templateParams 模板参数
*/
private void sendNotifyMessageToAdmin(Long userId, String templateCode,
java.util.Map<String, Object> templateParams) {
try {
NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO();
reqDTO.setUserId(userId);
reqDTO.setTemplateCode(templateCode);
reqDTO.setTemplateParams(templateParams);
notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO);
log.info("[站内信发送成功] userId={}, templateCode={}", userId, templateCode);
} catch (Exception e) {
log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e);
}
}
// ==================== 辅助方法 ====================
/**
* 获取区域名称
*/
private String getAreaName(Long areaId) {
// TODO: 从区域服务获取区域名称
return "某区域";
}
/**
* 记录暂停开始时间
*/
private void recordPauseStartTime(Long orderId) {
OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO();
updateObj.setOpsOrderId(orderId);
updateObj.setPauseStartTime(LocalDateTime.now());
cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj);
}
}

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());
}
}