diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/handler/CleanOrderEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/handler/CleanOrderEventHandler.java new file mode 100644 index 0000000..a096862 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/handler/CleanOrderEventHandler.java @@ -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; + +/** + * 保洁工单事件处理器 + *
+ * 职责: + * 1. 订阅工单状态变更事件,处理保洁业务特定逻辑 + * 2. 记录保洁扩展信息(到岗时间、完成时间、暂停时间等) + * 3. 更新保洁员状态 + * 4. 发送通知(语音播报、站内信、IoT震动) + *
+ * 设计说明: + * - 使用 @EventListener 订阅领域事件 + * - 业务逻辑同步执行 + * - 通知逻辑异步执行(@Async) + * - 消息内容使用 {@link CleanNotificationConstants} 统一管理 + *
+ * - 未来迁移 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; + + /** + * 站内信发送 API(Feign 客户端,微服务架构下调用 system 服务) + */ + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + + /** + * IoT 设备控制 API(Feign 客户端,微服务架构下调用 IoT 服务) + */ + @Resource + private IotDeviceControlApi iotDeviceControlApi; + + /** + * 订阅状态变更事件 + *
+ * 只处理保洁类型的工单(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
+ * 通过 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.
+ * 通过 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.
+ * 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
+ * 当工单完成时发布此事件
+ *
+ * @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
+ * 当新工单创建时发布此事件
+ *
+ * @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
+ * 职责:
+ * 1. 发布工单状态变更事件
+ * 2. 发布工单创建事件
+ * 3. 发布工单完成事件
+ *
+ * 设计说明:
+ * - 使用领域事件模式解耦状态机与业务逻辑
+ * - 业务方通过 @EventListener 订阅事件
+ * - 事件异步发布,不阻塞主流程
+ *
+ * @author lzh
+ */
+public interface OrderEventPublisher {
+
+ /**
+ * 发布状态变更事件
+ *
+ * @param event 状态变更事件
+ */
+ void publishStateChanged(OrderStateChangedEvent event);
+
+ /**
+ * 发布工单创建事件
+ *
+ * @param event 工单创建事件
+ */
+ void publishOrderCreated(OrderCreatedEvent event);
+
+ /**
+ * 发布工单完成事件
+ *
+ * @param event 工单完成事件
+ */
+ void publishOrderCompleted(OrderCompletedEvent event);
+}
diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderEventPublisherImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderEventPublisherImpl.java
new file mode 100644
index 0000000..eac8a90
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderEventPublisherImpl.java
@@ -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;
+
+/**
+ * 工单事件发布器实现
+ *
+ * 使用 Spring 的 ApplicationEventPublisher 发布事件
+ * 业务方通过 @EventListener 订阅事件
+ *
+ * 使用示例:
+ *
+ * 当工单状态发生变更时发布此事件,业务方订阅此事件处理自己的逻辑
+ *
+ * 设计说明:
+ * - 使用领域事件模式解耦状态机与业务逻辑
+ * - 业务方通过 @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
* 职责:
* 1. 管理工单状态转换规则
* 2. 执行状态转换并记录事件
- * 3. 触发状态变更监听器
+ * 3. 发布状态变更事件(通过事件发布器)
+ *
+ * 设计原则:
+ * - 单一职责:只负责状态转换规则验证和状态管理
+ * - 不触发业务逻辑:业务逻辑通过事件订阅处理
+ * - 事件驱动:状态变更后发布事件,业务方订阅处理
+ *
+ * 变更说明:
+ * - 移除了监听器机制(改为事件发布)
+ * - 状态机只负责状态转换验证和状态更新
+ * - 业务逻辑通过 @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
+ * 此方法负责:
+ * 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
+ * 替代原有的监听器机制:
+ * - 业务方通过 @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
+ * @Component
+ * public class CleanOrderEventHandler {
+ * @EventListener
+ * public void onStateChanged(OrderStateChangedEvent event) {
+ * // 处理保洁工单状态变更
+ * if ("CLEAN".equals(event.getOrderType())) {
+ * // ...
+ * }
+ * }
+ * }
+ *
+ *
+ * @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);
+ }
+ }
+}
diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderStateChangedEvent.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderStateChangedEvent.java
new file mode 100644
index 0000000..d40015a
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderStateChangedEvent.java
@@ -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;
+
+/**
+ * 工单状态变更领域事件
+ *