From 5e9dc8b1046bbbad9e4aa55c73beac052f22abc5 Mon Sep 17 00:00:00 2001 From: lzh Date: Thu, 8 Jan 2026 15:05:09 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E3=80=90ops=E3=80=91=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9C=BA=E5=AE=9E=E7=8E=B0/=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E4=BA=8B=E4=BB=B6=E5=8F=91=E5=B8=83/?= =?UTF-8?q?=E4=BF=9D=E6=B4=81=E7=9B=91=E5=90=AC=E5=A4=84=E7=90=86=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/CleanOrderEventHandler.java | 603 ++++++++++++++++++ .../ops/core/event/OrderCompletedEvent.java | 71 +++ .../ops/core/event/OrderCreatedEvent.java | 77 +++ .../ops/core/event/OrderEventPublisher.java | 40 ++ .../core/event/OrderEventPublisherImpl.java | 71 +++ .../core/event/OrderStateChangedEvent.java | 172 +++++ .../ops/service/fsm/OrderStateMachine.java | 291 +++++---- 7 files changed, 1187 insertions(+), 138 deletions(-) create mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/handler/CleanOrderEventHandler.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderCompletedEvent.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderCreatedEvent.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderEventPublisher.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderEventPublisherImpl.java create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderStateChangedEvent.java 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 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 设备操作方法 ==================== + + /** + * 语音播报 + *

+ * 通过 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.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); + } + } + + /** + * 震动提醒 + *

+ * 通过 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.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 + *

+ * 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 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 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); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderCompletedEvent.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderCompletedEvent.java new file mode 100644 index 0000000..19f5815 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderCompletedEvent.java @@ -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; + +/** + * 工单完成领域事件 + *

+ * 当工单完成时发布此事件 + * + * @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 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; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderCreatedEvent.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderCreatedEvent.java new file mode 100644 index 0000000..188e59d --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderCreatedEvent.java @@ -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; + +/** + * 工单创建领域事件 + *

+ * 当新工单创建时发布此事件 + * + * @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 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; + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderEventPublisher.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderEventPublisher.java new file mode 100644 index 0000000..f4ba2e5 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/event/OrderEventPublisher.java @@ -0,0 +1,40 @@ +package com.viewsh.module.ops.core.event; + +/** + * 工单事件发布器接口 + *

+ * 职责: + * 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 订阅事件 + *

+ * 使用示例: + *

+ * @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; + +/** + * 工单状态变更领域事件 + *

+ * 当工单状态发生变更时发布此事件,业务方订阅此事件处理自己的逻辑 + *

+ * 设计说明: + * - 使用领域事件模式解耦状态机与业务逻辑 + * - 业务方通过 @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 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 getPayload(String key, Class 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(); + } +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/fsm/OrderStateMachine.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/fsm/OrderStateMachine.java index c1c30fc..e486909 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/fsm/OrderStateMachine.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/fsm/OrderStateMachine.java @@ -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.*; /** * 工单状态机核心 + *

* 职责: * 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> 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 listeners = new ArrayList<>(); - - /** - * 注册监听器 - * - * @param listener 监听器实例 - */ - public void registerListener(OrderStateChangeListener listener) { - if (listener != null) { - listeners.add(listener); - log.info("注册状态监听器: {}", listener.getClass().getSimpleName()); - } - } - /** * 执行状态转换(核心方法) + *

+ * 此方法负责: + * 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 allowedTargets = TRANSITIONS.get(fromStatus); + return allowedTargets != null && allowedTargets.contains(toStatus); + } + + /** + * 获取当前状态允许转换到的目标状态集合 + * + * @param currentStatus 当前状态 + * @return 允许的目标状态集合 + */ + public Set getAllowedTransitions(WorkOrderStatusEnum currentStatus) { + return TRANSITIONS.getOrDefault(currentStatus, Collections.emptySet()); + } + + // ==================== 私有方法 ==================== + /** * 校验状态转换是否合法 */ private void validateTransition(WorkOrderStatusEnum currentStatus, WorkOrderStatusEnum newStatus) { - Set 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 { } /** - * 触发所有监听器 + * 发布状态变更事件 + *

+ * 替代原有的监听器机制: + * - 业务方通过 @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 getAllowedTransitions(WorkOrderStatusEnum currentStatus) { - return TRANSITIONS.getOrDefault(currentStatus, Collections.emptySet()); - } - }