diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java index 2efdbb4..4d03cbb 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java @@ -1,8 +1,7 @@ package com.viewsh.module.ops.environment.integration.listener; -import cn.hutool.core.map.MapUtil; import com.viewsh.module.iot.api.device.IotDeviceControlApi; -import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO; +import com.viewsh.module.iot.api.device.dto.ResetTrafficCounterReqDTO; import com.viewsh.module.ops.core.dispatch.DispatchEngine; import com.viewsh.module.ops.core.dispatch.model.DispatchResult; import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; @@ -15,9 +14,11 @@ import com.viewsh.module.ops.enums.PriorityEnum; import com.viewsh.module.ops.environment.constants.CleanNotificationConstants; import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO; import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper; -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.VoiceBroadcastService; +import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; import com.viewsh.module.system.api.notify.NotifyMessageSendApi; import com.viewsh.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import jakarta.annotation.Resource; @@ -28,6 +29,7 @@ import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; import java.time.LocalDateTime; +import java.util.Map; /** * 保洁工单事件监听器(统一入口) @@ -36,12 +38,19 @@ import java.time.LocalDateTime; * 1. 监听工单创建事件,触发自动调度 * 2. 监听工单状态变更事件,处理: * - 扩展表时间记录(到岗、完成、暂停) - * - 保洁员状态同步 - * - 通知发送(语音、震动、站内信) + * - 通知发送(语音、站内信) + * - 业务日志记录 * 3. 监听工单完成事件,触发自动派单下一个任务 *

+ * 职责划分: + * - 设备状态管理:由 BadgeDeviceStatusServiceImpl 处理 + * - 扩展表时间记录:由 CleanOrderEventListener 处理 + * - 通知发送:由 CleanOrderEventListener 处理 + * - 业务日志记录:由 CleanOrderEventListener 处理 + * - 自动调度:由 CleanOrderEventListener 触发 + *

* 设计说明: - * - 整合了 OrderCreatedEventListener、CleanerStateChangeListener、CleanOrderEventHandler 的功能 + * - assigneeId 存储的是工牌设备ID(非保洁员ID) * - 使用异步处理,避免阻塞主流程 * - 使用 @TransactionalEventListener(AFTER_COMMIT) 确保事务提交后再处理 * @@ -60,9 +69,6 @@ public class CleanOrderEventListener { @Resource private OpsOrderCleanExtMapper cleanExtMapper; - @Resource - private CleanerStatusService cleanerStatusService; - @Resource private CleanOrderService cleanOrderService; @@ -75,13 +81,13 @@ public class CleanOrderEventListener { @Resource private IotDeviceControlApi iotDeviceControlApi; + @Resource + private EventLogRecorder eventLogRecorder; + // ==================== 工单创建事件 ==================== /** * 监听工单创建事件,触发自动调度 - *

- * 使用 @TransactionalEventListener 确保在事务提交后才执行调度 - * 避免调度失败导致工单创建回滚 */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { @@ -94,6 +100,11 @@ public class CleanOrderEventListener { // 异步触发调度 asyncDispatchAfterCreated(event); + + // 如果是客流触发的工单,重置客流计数器 + if ("IOT_TRAFFIC".equals(event.getPayload().get("triggerSource"))) { + asyncResetTrafficCounter(event); + } } /** @@ -122,7 +133,7 @@ public class CleanOrderEventListener { DispatchResult result = dispatchEngine.dispatch(context); if (result.isSuccess()) { - log.info("[CleanOrderEventListener] 自动调度成功: orderId={}, assigneeId={}", + log.info("[CleanOrderEventListener] 自动调度成功: orderId={}, deviceId={}", event.getOrderId(), result.getAssigneeId()); } else { log.warn("[CleanOrderEventListener] 自动调度失败: orderId={}, reason={}", @@ -134,10 +145,63 @@ public class CleanOrderEventListener { } } + /** + * 异步重置客流计数器 + */ + @Async("ops-task-executor") + public void asyncResetTrafficCounter(OrderCreatedEvent event) { + try { + Long deviceId = (Long) event.getPayload().get("triggerDeviceId"); + @SuppressWarnings("unchecked") + Map triggerData = (Map) event.getPayload().get("triggerData"); + + if (deviceId == null || triggerData == null) { + log.warn("[CleanOrderEventListener] 缺少客流数据,跳过重置: orderId={}, deviceId={}, triggerData={}", + event.getOrderId(), deviceId, triggerData); + return; + } + + Object actualCountObj = triggerData.get("actualCount"); + if (actualCountObj == null) { + log.warn("[CleanOrderEventListener] 缺少客流值,跳过重置: orderId={}", event.getOrderId()); + return; + } + + // 将当前客流值设为新的基准值 + Long newBaseValue = ((Number) actualCountObj).longValue(); + + // 构建重置请求 + ResetTrafficCounterReqDTO reqDTO = ResetTrafficCounterReqDTO.builder() + .deviceId(deviceId) + .newBaseValue(newBaseValue) + .orderId(event.getOrderId()) + .remark("工单创建后重置计数器") + .build(); + + // 调用 IoT 模块 RPC 接口 + var result = iotDeviceControlApi.resetTrafficCounter(reqDTO); + + if (result.getData() != null && result.getData()) { + log.info("[CleanOrderEventListener] 客流计数器重置成功: orderId={}, deviceId={}, newBaseValue={}", + event.getOrderId(), deviceId, newBaseValue); + } else { + log.warn("[CleanOrderEventListener] 客流计数器重置失败: orderId={}, deviceId={}", + event.getOrderId(), deviceId); + } + + } catch (Exception e) { + // 重置失败不应影响主流程 + log.error("[CleanOrderEventListener] 客流计数器重置异常: orderId={}", event.getOrderId(), e); + } + } + // ==================== 状态变更事件 ==================== /** * 监听状态变更事件 + *

+ * 注意:设备状态由 BadgeDeviceStatusServiceImpl 统一管理 + * 这里只处理扩展表时间记录和通知发送 */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderStateChanged(OrderStateChangedEvent event) { @@ -145,8 +209,8 @@ public class CleanOrderEventListener { return; } - log.info("[CleanOrderEventListener] 状态变更: orderId={}, {} -> {}, operatorId={}", - event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId()); + log.info("[CleanOrderEventListener] 状态变更: orderId={}, {} -> {}", + event.getOrderId(), event.getOldStatus(), event.getNewStatus()); switch (event.getNewStatus()) { case DISPATCHED: @@ -165,7 +229,8 @@ public class CleanOrderEventListener { handleCompleted(event); break; case CANCELLED: - handleCancelled(event); + // 设备状态由 BadgeDeviceStatusEventListener 统一处理 + log.debug("[CleanOrderEventListener] CANCELLED 状态已处理: orderId={}", event.getOrderId()); break; default: break; @@ -177,8 +242,28 @@ public class CleanOrderEventListener { */ @Async("ops-task-executor") public void handleDispatched(OrderStateChangedEvent event) { - // 发送新工单通知(语音+震动+站内信) - sendNewOrderNotification(event.getOperatorId(), event.getOrderId()); + Long orderId = event.getOrderId(); + + // 1. 记录下发时间到扩展表(用于计算响应时长) + recordDispatchedTime(orderId); + + // 2. 如果是从 PAUSED 恢复,处理暂停结束逻辑 + if (event.getOldStatus() == com.viewsh.module.ops.enums.WorkOrderStatusEnum.PAUSED) { + handlePauseEnd(orderId); + } + + // 3. 优先使用 assigneeId(自动调度场景),其次使用 operatorId(手动派单场景) + Long deviceId = event.getPayloadLong("assigneeId"); + if (deviceId == null) { + deviceId = event.getOperatorId(); + } + if (deviceId != null) { + // 发送新工单通知(语音+站内信) + sendNewOrderNotification(deviceId, orderId); + } else { + log.warn("[CleanOrderEventListener] DISPATCHED 事件缺少 assigneeId 和 operatorId: orderId={}", + orderId); + } } /** @@ -186,21 +271,25 @@ public class CleanOrderEventListener { */ @Async("ops-task-executor") public void handleConfirmed(OrderStateChangedEvent event) { - Long cleanerId = event.getOperatorId(); - if (cleanerId == null) { - return; + Long orderId = event.getOrderId(); + + // 获取 deviceId(优先从 payload 获取,其次使用 operatorId) + Long deviceId = event.getPayloadLong("deviceId"); + if (deviceId == null) { + deviceId = event.getOperatorId(); } - // 更新保洁员状态为 BUSY - cleanerStatusService.updateStatus(cleanerId, - com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY, - "确认工单"); + // 记录业务日志 + recordOrderConfirmedLog(orderId, deviceId, event); - // 设置当前工单 - cleanerStatusService.setCurrentWorkOrder(cleanerId, event.getOrderId(), - opsOrderMapper.selectById(event.getOrderId()).getOrderCode()); + if (deviceId != null) { + // 发送确认成功语音播报(使用统一模板) + OpsOrderDO order = opsOrderMapper.selectById(orderId); + String confirmMessage = CleanNotificationConstants.VoiceBuilder.buildOrderConfirmed(order); + playVoice(deviceId, confirmMessage); + } - log.info("[CleanOrderEventListener] 保洁员确认工单,状态更新为BUSY: cleanerId={}", cleanerId); + log.info("[CleanOrderEventListener] 工单已确认: orderId={}, deviceId={}", orderId, deviceId); } /** @@ -216,35 +305,28 @@ public class CleanOrderEventListener { updateObj.setArrivedTime(LocalDateTime.now()); cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); - log.info("[CleanOrderEventListener] 保洁员已到岗: orderId={}", orderId); + // 2. 计算并更新响应时长(下发→到岗,排除暂停时间) + updateResponseSeconds(orderId); + + log.info("[CleanOrderEventListener] 到岗时间已记录: orderId={}", orderId); } /** * 处理暂停状态 + *

+ * 注意:PAUSED 状态不是由 IoT 消息触发的,而是通过服务层 (REST API) 调用: + * - OrderLifecycleManager.pauseOrder() - 用户主动暂停 + * - OrderLifecycleManager.interruptOrder() - P0任务打断 */ @Async("ops-task-executor") public void handlePaused(OrderStateChangedEvent event) { Long orderId = event.getOrderId(); - Long operatorId = event.getOperatorId(); - if (operatorId == null) { - return; - } - // 检查是否是被 P0 任务打断 - String interruptReason = event.getPayloadString("interruptReason"); - if ("P0_TASK_INTERRUPT".equals(interruptReason)) { - log.warn("[CleanOrderEventListener] 保洁任务被P0任务打断: orderId={}", orderId); - // 释放保洁员资源 - cleanerStatusService.clearCurrentWorkOrder(operatorId); - } else { - // 普通暂停:更新保洁员状态为 PAUSED - cleanerStatusService.updateStatus(operatorId, - com.viewsh.module.ops.enums.CleanerStatusEnum.PAUSED, - event.getRemark() != null ? event.getRemark() : "任务暂停"); - } - - // 记录暂停开始时间 + // 记录暂停开始时间到扩展表 recordPauseStartTime(orderId); + + // 设备状态由 BadgeDeviceStatusEventListener 统一处理 + log.info("[CleanOrderEventListener] 暂停时间已记录: orderId={}", orderId); } /** @@ -254,35 +336,27 @@ public class CleanOrderEventListener { public void handleCompleted(OrderStateChangedEvent event) { Long orderId = event.getOrderId(); - // 1. 计算作业时长 - Integer actualDuration = cleanOrderService.calculateActualDuration(orderId); + // 获取 deviceId(优先从 payload 获取,其次从工单获取) + Long deviceId = event.getPayloadLong("deviceId"); - // 2. 记录完成时间到扩展表 + // 记录完成时间到扩展表 OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); updateObj.setOpsOrderId(orderId); updateObj.setCompletedTime(LocalDateTime.now()); cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); - log.info("[CleanOrderEventListener] 保洁作业完成: orderId={}, actualDuration={}秒", orderId, actualDuration); - } + // 记录业务日志 + recordOrderCompletedLog(orderId, deviceId, event); - /** - * 处理取消状态 - */ - @Async("ops-task-executor") - public void handleCancelled(OrderStateChangedEvent event) { - Long operatorId = event.getOperatorId(); - if (operatorId != null) { - // 清理保洁员当前工单 - cleanerStatusService.clearCurrentWorkOrder(operatorId); - } - log.info("[CleanOrderEventListener] 保洁工单已取消: orderId={}", event.getOrderId()); + log.info("[CleanOrderEventListener] 完成时间已记录: orderId={}, deviceId={}", orderId, deviceId); } // ==================== 工单完成事件 ==================== /** * 监听工单完成事件 + *

+ * 触发自动调度下一个任务 */ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCompleted(OrderCompletedEvent event) { @@ -290,19 +364,16 @@ public class CleanOrderEventListener { return; } - log.info("[CleanOrderEventListener] 工单完成: orderId={}, assigneeId={}", + log.info("[CleanOrderEventListener] 工单完成: orderId={}, deviceId={}", event.getOrderId(), event.getAssigneeId()); - Long assigneeId = event.getAssigneeId(); - if (assigneeId != null) { - // 1. 清理当前工单 - cleanerStatusService.clearCurrentWorkOrder(assigneeId); - - // 2. 自动推送下一个任务(异步) - asyncDispatchNext(assigneeId, event.getOrderId()); + Long deviceId = event.getAssigneeId(); + if (deviceId != null) { + // 自动推送下一个任务(异步) + asyncDispatchNext(deviceId, event.getOrderId()); } - // 3. 发送完成通知(异步) + // 发送完成通知(异步) asyncSendOrderCompletedNotification(event.getOrderId()); } @@ -310,8 +381,8 @@ public class CleanOrderEventListener { * 异步推送下一个任务 */ @Async("ops-task-executor") - public void asyncDispatchNext(Long cleanerId, Long completedOrderId) { - cleanOrderService.autoDispatchNextOrder(completedOrderId, cleanerId); + public void asyncDispatchNext(Long deviceId, Long completedOrderId) { + cleanOrderService.autoDispatchNextOrder(completedOrderId, deviceId); } /** @@ -325,10 +396,10 @@ public class CleanOrderEventListener { // ==================== 通知方法 ==================== /** - * 发送新工单通知(语音播报 + 震动提醒 + 站内信) + * 发送新工单通知(语音播报 + 站内信) */ @Async("ops-task-executor") - public void sendNewOrderNotification(Long cleanerId, Long orderId) { + public void sendNewOrderNotification(Long deviceId, Long orderId) { try { OpsOrderDO order = opsOrderMapper.selectById(orderId); if (order == null) { @@ -336,25 +407,23 @@ public class CleanOrderEventListener { return; } - log.info("[新工单通知] cleanerId={}, orderId={}", cleanerId, orderId); + log.info("[新工单通知] deviceId={}, orderId={}", deviceId, orderId); - // 1. 语音播报 - playVoice(cleanerId, CleanNotificationConstants.VoiceMessage.NEW_ORDER); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildNewOrder(order, true); + playVoice(deviceId, voiceMessage); - // 2. 震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, + // 2. 发送站内信(暂时发送到管理员) + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.NEW_ORDER, CleanNotificationConstants.NotifyParamsBuilder.newOrderParams( order.getOrderCode(), order.getTitle(), - getAreaName(order.getAreaId()) + CleanNotificationConstants.VoiceBuilder.getAreaName(order.getLocation()) )); } catch (Exception e) { - log.error("[新工单通知] 发送失败: cleanerId={}, orderId={}", cleanerId, orderId, e); + log.error("[新工单通知] 发送失败: deviceId={}, orderId={}", deviceId, orderId, e); } } @@ -362,26 +431,23 @@ public class CleanOrderEventListener { * 发送待办增加通知 */ @Async("ops-task-executor") - public void sendQueuedOrderNotification(Long cleanerId, int queueCount) { + public void sendQueuedOrderNotification(Long deviceId, int queueCount) { try { - log.info("[待办增加通知] cleanerId={}, queueCount={}", cleanerId, queueCount); + log.info("[待办增加通知] deviceId={}, queueCount={}", deviceId, queueCount); - // 1. 语音播报 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatQueuedOrder(queueCount, 1); - playVoice(cleanerId, voiceMessage); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildQueuedOrder(queueCount); + playVoice(deviceId, voiceMessage); - // 2. 震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.LIGHT); - - // 3. 发送站内信(待办数量较多时) + // 2. 发送站内信(待办数量较多时) if (queueCount >= 3) { - sendNotifyMessageToMember(cleanerId, + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.QUEUED_ORDER, CleanNotificationConstants.NotifyParamsBuilder.queuedOrderParams(queueCount, 1)); } } catch (Exception e) { - log.error("[待办增加通知] 发送失败: cleanerId={}", cleanerId, e); + log.error("[待办增加通知] 发送失败: deviceId={}", deviceId, e); } } @@ -389,24 +455,21 @@ public class CleanOrderEventListener { * 发送下一个任务通知 */ @Async("ops-task-executor") - public void sendNextTaskNotification(Long cleanerId, int queueCount, String orderTitle) { + public void sendNextTaskNotification(Long deviceId, int queueCount, String orderTitle) { try { - log.info("[下一任务通知] cleanerId={}, queueCount={}, title={}", cleanerId, queueCount, orderTitle); + log.info("[下一任务通知] deviceId={}, queueCount={}, title={}", deviceId, queueCount, orderTitle); - // 1. 语音播报 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatNextTask(queueCount, orderTitle); - playVoice(cleanerId, voiceMessage); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildNextTask(orderTitle); + playVoice(deviceId, voiceMessage); - // 2. 震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, + // 2. 发送站内信 + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.NEXT_TASK, CleanNotificationConstants.NotifyParamsBuilder.nextTaskParams(queueCount, orderTitle)); } catch (Exception e) { - log.error("[下一任务通知] 发送失败: cleanerId={}", cleanerId, e); + log.error("[下一任务通知] 发送失败: deviceId={}", deviceId, e); } } @@ -414,24 +477,21 @@ public class CleanOrderEventListener { * 发送P0紧急任务插队通知 */ @Async("ops-task-executor") - public void sendPriorityUpgradeNotification(Long cleanerId, String orderCode) { + public void sendPriorityUpgradeNotification(Long deviceId, String orderCode) { try { - log.warn("[P0紧急通知] cleanerId={}, orderCode={}", cleanerId, orderCode); + log.warn("[P0紧急通知] deviceId={}, orderCode={}", deviceId, orderCode); - // 1. 语音播报 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatPriorityUpgrade(orderCode); - playVoice(cleanerId, voiceMessage); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildPriorityUpgrade(orderCode); + playVoice(deviceId, voiceMessage); - // 2. 强烈震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.STRONG); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, + // 2. 发送站内信 + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.PRIORITY_UPGRADE, CleanNotificationConstants.NotifyParamsBuilder.priorityUpgradeParams(orderCode, "P0紧急任务")); } catch (Exception e) { - log.error("[P0紧急通知] 发送失败: cleanerId={}", cleanerId, e); + log.error("[P0紧急通知] 发送失败: deviceId={}", deviceId, e); } } @@ -439,24 +499,21 @@ public class CleanOrderEventListener { * 发送任务恢复通知 */ @Async("ops-task-executor") - public void sendTaskResumedNotification(Long cleanerId, String areaName) { + public void sendTaskResumedNotification(Long deviceId, String areaName) { try { - log.info("[任务恢复通知] cleanerId={}, areaName={}", cleanerId, areaName); + log.info("[任务恢复通知] deviceId={}, areaName={}", deviceId, areaName); - // 1. 语音播报 - String voiceMessage = CleanNotificationConstants.VoiceHelper.formatTaskResumed(areaName); - playVoice(cleanerId, voiceMessage); + // 1. 语音播报(使用统一模板构建器) + String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildTaskResumed(areaName); + playVoice(deviceId, voiceMessage); - // 2. 震动提醒 - vibrate(cleanerId, CleanNotificationConstants.VibrationDuration.NORMAL); - - // 3. 发送站内信 - sendNotifyMessageToMember(cleanerId, + // 2. 发送站内信 + sendNotifyMessage(1L, CleanNotificationConstants.TemplateCode.TASK_RESUMED, CleanNotificationConstants.NotifyParamsBuilder.taskResumedParams(areaName)); } catch (Exception e) { - log.error("[任务恢复通知] 发送失败: cleanerId={}", cleanerId, e); + log.error("[任务恢复通知] 发送失败: deviceId={}", deviceId, e); } } @@ -485,73 +542,31 @@ public class CleanOrderEventListener { * 播放语音(供外部调用) */ @Async("ops-task-executor") - public void playVoiceForNewOrder(Long cleanerId) { - playVoice(cleanerId, CleanNotificationConstants.VoiceMessage.NEW_ORDER); + public void playVoiceForNewOrder(Long deviceId) { + playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.NEW_ORDER_SHORT); } - // ==================== IoT 设备操作���法 ==================== + // ==================== 设备操作方法 ==================== /** * 语音播报 */ - private void playVoice(Long cleanerId, String message) { + private void playVoice(Long deviceId, String message) { try { - Long deviceId = getBadgeDeviceId(cleanerId); - if (deviceId == null) { - log.warn("[语音播报] 保洁员无关联工牌设备: cleanerId={}", cleanerId); - return; - } - voiceBroadcastService.broadcast(deviceId, message); - log.debug("[语音播报] 调用成功: cleanerId={}, deviceId={}, message={}", cleanerId, deviceId, message); + log.debug("[语音播报] 调用成功: deviceId={}, message={}", deviceId, message); } catch (Exception e) { - log.error("[语音播报] 调用失败: cleanerId={}, message={}", cleanerId, message, e); + log.error("[语音播报] 调用失败: deviceId={}, message={}", deviceId, message, e); } } - /** - * 震动提醒 - */ - private void vibrate(Long cleanerId, int durationMs) { - try { - Long deviceId = getBadgeDeviceId(cleanerId); - if (deviceId == null) { - return; - } - - IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO(); - reqDTO.setDeviceId(deviceId); - reqDTO.setIdentifier("vibrate"); - reqDTO.setParams(MapUtil.builder() - .put("duration", durationMs) - .put("intensity", 50) - .build()); - - iotDeviceControlApi.invokeService(reqDTO); - log.debug("[震动提醒] 调用成功: cleanerId={}, durationMs={}", cleanerId, durationMs); - - } catch (Exception e) { - log.error("[震动提醒] 调用失败: cleanerId={}, durationMs={}", cleanerId, durationMs, e); - } - } - - /** - * 获取保洁员关联的工牌设备ID - * TODO: 需要实现 - */ - private Long getBadgeDeviceId(Long cleanerId) { - // TODO: 实现获取保洁员关联的工牌设备ID - return null; - } - // ==================== 站内信发送方法 ==================== /** - * 发送站内信给保洁员 + * 发送站内信 */ - private void sendNotifyMessageToMember(Long userId, String templateCode, - java.util.Map templateParams) { + private void sendNotifyMessage(Long userId, String templateCode, Map templateParams) { try { NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO(); reqDTO.setUserId(userId); @@ -566,25 +581,6 @@ public class CleanOrderEventListener { } } - /** - * 发送站内信给管理员 - */ - 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.debug("[站内信发送成功] userId={}, templateCode={}", userId, templateCode); - - } catch (Exception e) { - log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e); - } - } - // ==================== 辅助方法 ==================== /** @@ -604,4 +600,167 @@ public class CleanOrderEventListener { updateObj.setPauseStartTime(LocalDateTime.now()); cleanExtMapper.insertOnDuplicateKeyUpdate(updateObj); } + + /** + * 记录下发时间 + */ + private void recordDispatchedTime(Long orderId) { + // 先查询现有记录 + OpsOrderCleanExtDO existing = cleanExtMapper.selectByOpsOrderId(orderId); + LocalDateTime now = LocalDateTime.now(); + + if (existing == null || existing.getFirstDispatchedTime() == null) { + // 首次下发,记录首次下发时间 + OpsOrderCleanExtDO updateObj = OpsOrderCleanExtDO.builder() + .opsOrderId(orderId) + .dispatchedTime(now) + .firstDispatchedTime(now) + .build(); + cleanExtMapper.insertOrUpdateSelective(updateObj); + log.info("[CleanOrderEventListener] 首次下发时间已记录: orderId={}", orderId); + } else { + // 非首次下发,只更新最近下发时间 + OpsOrderCleanExtDO updateObj = OpsOrderCleanExtDO.builder() + .opsOrderId(orderId) + .dispatchedTime(now) + .firstDispatchedTime(existing.getFirstDispatchedTime()) // 保留首次时间 + .build(); + cleanExtMapper.insertOrUpdateSelective(updateObj); + log.debug("[CleanOrderEventListener] 下发时间已更新: orderId={}", orderId); + } + } + + /** + * 处理暂停结束 + *

+ * 计算暂停时长并累加到 totalPauseSeconds + */ + private void handlePauseEnd(Long orderId) { + try { + OpsOrderCleanExtDO ext = cleanExtMapper.selectByOpsOrderId(orderId); + if (ext == null || ext.getPauseStartTime() == null) { + log.warn("[CleanOrderEventListener] 暂停开始时间不存在,无法计算暂停时长: orderId={}", orderId); + return; + } + + // 计算暂停时长(秒) + LocalDateTime pauseEnd = LocalDateTime.now(); + long pauseSeconds = java.time.Duration.between(ext.getPauseStartTime(), pauseEnd).getSeconds(); + + // 获取现有累计暂停时长 + int totalPauseSeconds = ext.getTotalPauseSeconds() != null ? ext.getTotalPauseSeconds() : 0; + totalPauseSeconds += (int) pauseSeconds; + + // 更新暂停结束时间和累计暂停时长 + OpsOrderCleanExtDO updateObj = OpsOrderCleanExtDO.builder() + .opsOrderId(orderId) + .pauseEndTime(pauseEnd) + .totalPauseSeconds(totalPauseSeconds) + .firstDispatchedTime(ext.getFirstDispatchedTime()) // 保留首次下发时间 + .build(); + cleanExtMapper.insertOrUpdateSelective(updateObj); + + log.info("[CleanOrderEventListener] 暂停时长已累加: orderId={}, 本次={}秒, 累计={}秒", + orderId, pauseSeconds, totalPauseSeconds); + + } catch (Exception e) { + log.error("[CleanOrderEventListener] 处理暂停结束失败: orderId={}", orderId, e); + } + } + + /** + * 计算并更新响应时长 + *

+ * 响应时长 = 到岗时间 - 首次下发时间 - 累计暂停时长 + */ + private void updateResponseSeconds(Long orderId) { + try { + OpsOrderCleanExtDO ext = cleanExtMapper.selectByOpsOrderId(orderId); + if (ext == null || ext.getFirstDispatchedTime() == null || ext.getArrivedTime() == null) { + log.warn("[CleanOrderEventListener] 缺少必要时间数据,无法计算响应时长: orderId={}", orderId); + return; + } + + // 计算响应时长(秒):到岗 - 首次下发 - 暂停时长 + long responseSeconds = java.time.Duration.between(ext.getFirstDispatchedTime(), ext.getArrivedTime()).getSeconds(); + int totalPauseSeconds = ext.getTotalPauseSeconds() != null ? ext.getTotalPauseSeconds() : 0; + responseSeconds = responseSeconds - totalPauseSeconds; + + // 确保不为负数 + responseSeconds = Math.max(responseSeconds, 0); + + // 更新工单主表的响应时长 + OpsOrderDO orderUpdate = new OpsOrderDO(); + orderUpdate.setId(orderId); + orderUpdate.setResponseSeconds((int) responseSeconds); + opsOrderMapper.updateById(orderUpdate); + + log.info("[CleanOrderEventListener] 响应时长已更新: orderId={}, 响应时长={}秒", orderId, responseSeconds); + + } catch (Exception e) { + log.error("[CleanOrderEventListener] 更新响应时长失败: orderId={}", orderId, e); + } + } + + /** + * 记录工单确认业务日志 + */ + private void recordOrderConfirmedLog(Long orderId, Long deviceId, OrderStateChangedEvent event) { + try { + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(EventDomain.DEVICE) + .eventType("ORDER_CONFIRM") + .message("工单已确认 (工牌按键)") + .targetId(orderId) + .targetType("order") + .deviceId(deviceId) + .personId(deviceId) + .build()); + + } catch (Exception e) { + log.warn("[CleanOrderEventListener] 记录确认业务日志失败: orderId={}", orderId, e); + } + } + + /** + * 记录工单完成业务日志 + */ + private void recordOrderCompletedLog(Long orderId, Long deviceId, OrderStateChangedEvent event) { + try { + // 从 payload 中提取完成原因和作业时长 + String triggerSource = (String) event.getPayload().get("triggerSource"); + String deviceKey = (String) event.getPayload().get("deviceKey"); + + // 构建日志消息 + String message = "工单已完成"; + if ("SIGNAL_LOSS_TIMEOUT".equals(triggerSource)) { + Object durationMs = event.getPayload().get("durationMs"); + String durationInfo = ""; + if (durationMs != null) { + long durationMinutes = ((Number) durationMs).longValue() / 60000; + durationInfo = String.format(",作业时长: %d分钟", durationMinutes); + } + message = "信号丢失超时自动完成 [设备:" + deviceKey + durationInfo + "]"; + } + + EventLogRecord.EventLogRecordBuilder builder = EventLogRecord.builder() + .module("clean") + .domain(EventDomain.BEACON) + .eventType("ORDER_COMPLETED") + .message(message) + .targetId(orderId) + .targetType("order"); + + if (deviceId != null) { + builder.deviceId(deviceId).personId(deviceId); + } + + eventLogRecorder.record(builder.build()); + + } catch (Exception e) { + log.warn("[CleanOrderEventListener] 记录完成业务日志失败: orderId={}", orderId, e); + } + } } +