refactor(ops): 工单事件监听器重构

- handleDispatched: 记录下发时间��处理暂停结束
- handlePaused: 记录暂停开始时间
- handleArrived: 记录到岗时间,计算响应时长
- handleConfirmed/handleCompleted: 使用 VoiceBuilder 播报
- 新增 recordDispatchedTime/handlePauseEnd/updateResponseSeconds 方法

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-01-25 18:22:07 +08:00
parent 3e23f9c65c
commit 95dffa0ba5

View File

@@ -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. 监听工单完成事件,触发自动派单下一个任务
* <p>
* 职责划分:
* - 设备状态管理:由 BadgeDeviceStatusServiceImpl 处理
* - 扩展表时间记录:由 CleanOrderEventListener 处理
* - 通知发送:由 CleanOrderEventListener 处理
* - 业务日志记录:由 CleanOrderEventListener 处理
* - 自动调度:由 CleanOrderEventListener 触发
* <p>
* 设计说明:
* - 整合了 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;
// ==================== 工单创建事件 ====================
/**
* 监听工单创建事件,触发自动调度
* <p>
* 使用 @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<String, Object> triggerData = (Map<String, Object>) 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);
}
}
// ==================== 状态变更事件 ====================
/**
* 监听状态变更事件
* <p>
* 注意:设备状态由 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);
}
/**
* 处理暂停状态
* <p>
* 注意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);
}
// ==================== 工单完成事件 ====================
/**
* 监听工单完成事件
* <p>
* 触发自动调度下一个任务
*/
@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 设备操作<EFBFBD><EFBFBD><EFBFBD>法 ====================
// ==================== 设备操作法 ====================
/**
* 语音播报
*/
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.<String, Object>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<String, Object> templateParams) {
private void sendNotifyMessage(Long userId, String templateCode, Map<String, Object> 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<String, Object> 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);
}
}
/**
* 处理暂停结束
* <p>
* 计算暂停时长并累加到 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);
}
}
/**
* 计算并更新响应时长
* <p>
* 响应时长 = 到岗时间 - 首次下发时间 - 累计暂停时长
*/
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);
}
}
}