refactor(clean): 抽取 CleanOrderNotificationService,解除 listener 循环依赖

- 通知方法(语音播报/站内信)从 CleanOrderEventListener 迁移到 CleanOrderNotificationService
- CleanOrderServiceImpl 改为依赖 NotificationService 而非 Listener
- CleanOrderEventListener 补齐 QUEUED 和 PAUSED 状态的 business log
- 派单/取消日志从 handleDispatched/handleCancelled 内联改为独立 recordXxxLog 方法
- 硬编码字符串 "CLEAN" 统一替换为 WorkOrderTypeEnum.CLEAN.getType()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-27 16:07:57 +08:00
parent 6e5366be57
commit 333329c29c
2 changed files with 171 additions and 258 deletions

View File

@@ -11,12 +11,16 @@ import com.viewsh.module.ops.core.event.OrderCreatedEvent;
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.PriorityEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.enums.WorkOrderTypeEnum;
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.dal.redis.TrafficActiveOrderRedisDAO;
import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService;
import com.viewsh.module.ops.environment.service.notification.CleanOrderNotificationService;
import com.viewsh.module.ops.environment.service.voice.TtsQueueMessage;
import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
@@ -24,8 +28,6 @@ import com.viewsh.module.ops.infrastructure.log.enumeration.LogModule;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
@@ -79,14 +81,9 @@ public class CleanOrderEventListener {
@Resource
private VoiceBroadcastService voiceBroadcastService;
@Resource
private NotifyMessageSendApi notifyMessageSendApi;
@Resource
private EventLogRecorder eventLogRecorder;
@Resource
private OrderQueueService orderQueueService;
@Resource
private TrafficActiveOrderRedisDAO trafficActiveOrderRedisDAO;
@@ -94,6 +91,9 @@ public class CleanOrderEventListener {
@Resource
private IotDeviceControlApi iotDeviceControlApi;
@Resource
private CleanOrderNotificationService cleanOrderNotificationService;
// ==================== 工单创建事件 ====================
/**
@@ -105,7 +105,7 @@ public class CleanOrderEventListener {
*/
@org.springframework.transaction.event.TransactionalEventListener(phase = org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
if (!"CLEAN".equals(event.getOrderType())) {
if (!WorkOrderTypeEnum.CLEAN.getType().equals(event.getOrderType())) {
return;
}
@@ -166,7 +166,7 @@ public class CleanOrderEventListener {
*/
@EventListener
public void onOrderStateChanged(OrderStateChangedEvent event) {
if (!"CLEAN".equals(event.getOrderType())) {
if (!WorkOrderTypeEnum.CLEAN.getType().equals(event.getOrderType())) {
return;
}
@@ -229,11 +229,14 @@ public class CleanOrderEventListener {
}
if (deviceId != null) {
// 发送新工单通知(语音+站内信)
sendNewOrderNotification(deviceId, orderId);
cleanOrderNotificationService.sendNewOrderNotification(deviceId, orderId);
} else {
log.warn("[CleanOrderEventListener] DISPATCHED 事件缺少 assigneeId 和 operatorId: orderId={}",
orderId);
}
// 4. 记录派单业务日志
recordDispatchedLog(orderId, deviceId, event);
}
/**
@@ -248,10 +251,29 @@ public class CleanOrderEventListener {
deviceId = event.getOperatorId();
}
if (deviceId != null) {
// TODO 入队语音播报暂时关闭,后续按需开启
log.info("[CleanOrderEventListener] 工单入队: deviceId={}, orderId={}", deviceId, orderId);
// 1. 业务日志
String assigneeName = (String) event.getPayload().get("assigneeName");
String message = assigneeName != null
? String.format("工单已入队,分配给 %s等待派发", assigneeName)
: "工单已入队等待派发";
try {
EventLogRecord.EventLogRecordBuilder builder = EventLogRecord.builder()
.module(LogModule.CLEAN)
.domain(EventDomain.DISPATCH)
.eventType(LogType.ORDER_QUEUED.getCode())
.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);
}
// 2. TODO 入队语音播报暂时关闭,后续按需开启
log.info("[CleanOrderEventListener] 工单入队: deviceId={}, orderId={}", deviceId, orderId);
}
/**
@@ -349,6 +371,34 @@ public class CleanOrderEventListener {
// 2. 记录暂停开始时间到扩展表
recordPauseStartTime(orderId);
// 3. 业务日志
String remark = event.getRemark();
Long urgentOrderId = event.getPayloadLong("urgentOrderId");
String message;
if (urgentOrderId != null) {
message = "P0紧急任务打断工单暂停";
} else {
message = "工单已暂停";
}
if (remark != null && !remark.isEmpty()) {
message += "" + remark + "";
}
try {
EventLogRecord.EventLogRecordBuilder builder = EventLogRecord.builder()
.module(LogModule.CLEAN)
.domain(EventDomain.DISPATCH)
.eventType(LogType.ORDER_PAUSED.getCode())
.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);
}
// 设备状态由 BadgeDeviceStatusEventListener 统一处理
log.info("[CleanOrderEventListener] 暂停处理完成: orderId={}, deviceId={}", orderId, deviceId);
}
@@ -441,7 +491,10 @@ public class CleanOrderEventListener {
TtsQueueMessage.TTS_FLAG_URGENT, orderId);
}
// 3. 自动调度下一个等待任务(如果有 assignee
// 3. 记录取消业务日志
recordCancelledLog(orderId, deviceId, event);
// 4. 自动调度下一个等待任务(如果有 assignee
if (deviceId != null) {
try {
cleanOrderService.autoDispatchNextOrder(orderId, deviceId);
@@ -463,7 +516,7 @@ public class CleanOrderEventListener {
*/
@EventListener
public void onOrderCompleted(OrderCompletedEvent event) {
if (!"CLEAN".equals(event.getOrderType())) {
if (!WorkOrderTypeEnum.CLEAN.getType().equals(event.getOrderType())) {
return;
}
@@ -491,238 +544,6 @@ public class CleanOrderEventListener {
cleanOrderService.autoDispatchNextOrder(orderId, deviceId);
}
// ==================== 通知方法 ====================
/**
* 发送新工单通知(循环语音播报 + 站内信)
*/
@Async("ops-task-executor")
public void sendNewOrderNotification(Long deviceId, Long orderId) {
try {
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
log.warn("[新工单通知] 工单不存在: orderId={}", orderId);
return;
}
log.info("[新工单通知] deviceId={}, orderId={}", deviceId, orderId);
// 1. 启动循环语音播报"工单来了"
voiceBroadcastService.broadcastLoop(deviceId,
CleanNotificationConstants.VoiceTemplate.NEW_ORDER_RING, orderId);
// 2. 发送站内信(暂时发送到管理员)
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.NEW_ORDER,
CleanNotificationConstants.NotifyParamsBuilder.newOrderParams(
order.getOrderCode(),
order.getTitle(),
CleanNotificationConstants.VoiceBuilder.getAreaName(order.getLocation())));
} catch (Exception e) {
log.error("[新工单通知] 发送失败: deviceId={}, orderId={}", deviceId, orderId, e);
}
}
/**
* 发送待办增加通知
*/
@Async("ops-task-executor")
public void sendQueuedOrderNotification(Long deviceId, int queueCount, Long orderId) {
try {
log.info("[待办增加通知] deviceId={}, queueCount={}", deviceId, queueCount);
// 1. 语音播报(使用统一模板构建器)
String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildQueuedOrder(queueCount);
playVoice(deviceId, voiceMessage, orderId);
// 2. 发送站内信(待办数量较多时)
if (queueCount >= 3) {
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.QUEUED_ORDER,
CleanNotificationConstants.NotifyParamsBuilder.queuedOrderParams(queueCount, 1));
}
} catch (Exception e) {
log.error("[待办增加通知] 发送失败: deviceId={}", deviceId, e);
}
}
/**
* 发送下一个任务通知
*/
@Async("ops-task-executor")
public void sendNextTaskNotification(Long deviceId, int queueCount, String orderTitle) {
try {
log.info("[下一任务通知] deviceId={}, queueCount={}, title={}", deviceId, queueCount, orderTitle);
// 1. 语音播报(使用统一模板构建器)
String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildNextTask(orderTitle);
playVoice(deviceId, voiceMessage);
// 2. 发送站内信
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.NEXT_TASK,
CleanNotificationConstants.NotifyParamsBuilder.nextTaskParams(queueCount, orderTitle));
} catch (Exception e) {
log.error("[下一任务通知] 发送失败: deviceId={}", deviceId, e);
}
}
/**
* 发送P0紧急任务插队通知
*/
@Async("ops-task-executor")
public void sendPriorityUpgradeNotification(Long deviceId, String orderCode, Long orderId) {
try {
log.warn("[P0紧急通知] deviceId={}, orderCode={}", deviceId, orderCode);
// 1. 语音播报P0紧急插队到队列头部
String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildPriorityUpgrade(orderCode);
playVoiceUrgent(deviceId, voiceMessage, orderId);
// 2. 发送站内信
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.PRIORITY_UPGRADE,
CleanNotificationConstants.NotifyParamsBuilder.priorityUpgradeParams(orderCode, "P0紧急任务"));
} catch (Exception e) {
log.error("[P0紧急通知] 发送失败: deviceId={}", deviceId, e);
}
}
/**
* 发送任务恢复通知
*/
@Async("ops-task-executor")
public void sendTaskResumedNotification(Long deviceId, String areaName) {
try {
log.info("[任务恢复通知] deviceId={}, areaName={}", deviceId, areaName);
// 1. 语音播报(使用统一模板构建器)
String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildTaskResumed(areaName);
playVoice(deviceId, voiceMessage);
// 2. 发送站内信
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.TASK_RESUMED,
CleanNotificationConstants.NotifyParamsBuilder.taskResumedParams(areaName));
} catch (Exception e) {
log.error("[任务恢复通知] 发送失败: deviceId={}", deviceId, e);
}
}
/**
* 发送工单完成通知(语音播报 + 站内信)
*/
public void sendOrderCompletedNotification(Long orderId, Long deviceId) {
try {
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
return;
}
log.info("[工单完成通知] orderId={}, orderCode={}, areaId={}, deviceId={}",
orderId, order.getOrderCode(), order.getAreaId(), deviceId);
// 1. 语音播报 - 通知保洁员工单已完成
if (deviceId != null) {
playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.ORDER_COMPLETED, orderId);
}
// 2. 发送站内信(给管理员)
sendNotifyMessage(1L,
CleanNotificationConstants.TemplateCode.ORDER_COMPLETED,
CleanNotificationConstants.NotifyParamsBuilder.orderCompletedParams(
order.getOrderCode(),
CleanNotificationConstants.VoiceBuilder.getAreaName(order.getLocation()),
order.getTitle()));
} catch (Exception e) {
log.error("[工单完成通知] 发送失败: orderId={}, deviceId={}", orderId, deviceId, e);
}
}
/**
* 播放语音(供外部调用)
*/
@Async("ops-task-executor")
public void playVoiceForNewOrder(Long deviceId) {
playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.NEW_ORDER_SHORT);
}
// ==================== 设备操作方法 ====================
/**
* 语音播报
*/
private void playVoice(Long deviceId, String message) {
playVoice(deviceId, message, null);
}
/**
* 语音播报带工单ID按序入队 FIFO
* <p>
* 大多数业务通知使用此方法,保证同一设备上的播报按入队顺序播放。
* 仅 P0 紧急插队场景使用 {@link #playVoiceUrgent}。
*/
private void playVoice(Long deviceId, String message, Long orderId) {
try {
voiceBroadcastService.broadcastInOrder(deviceId, message, orderId);
log.debug("[语音播报] 调用成功: deviceId={}, message={}", deviceId, message);
} catch (Exception e) {
log.error("[语音播报] 调用失败: deviceId={}, message={}", deviceId, message, e);
}
}
/**
* 紧急语音播报(插队到队列头部)
* <p>
* 仅用于 P0 紧急任务打断等需要立即播报的场景
*/
private void playVoiceUrgent(Long deviceId, String message, Long orderId) {
try {
voiceBroadcastService.broadcastUrgent(deviceId, message, orderId);
log.debug("[语音播报-紧急] 调用成功: deviceId={}, message={}", deviceId, message);
} catch (Exception e) {
log.error("[语音播报-紧急] 调用失败: deviceId={}, message={}", deviceId, message, e);
}
}
// ==================== 站内信发送方法 ====================
/**
* 发送站内信
*/
private void sendNotifyMessage(Long userId, String templateCode, Map<String, Object> templateParams) {
try {
NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO();
reqDTO.setUserId(userId);
reqDTO.setTemplateCode(templateCode);
reqDTO.setTemplateParams(templateParams);
notifyMessageSendApi.sendSingleMessageToMember(reqDTO);
log.debug("[站内信发送成功] userId={}, templateCode={}", userId, templateCode);
} catch (Exception e) {
log.error("[站内信发送失败] userId={}, templateCode={}", userId, templateCode, e);
}
}
// ==================== 辅助方法 ====================
/**
* 获取区域名称
*/
private String getAreaName(Long areaId) {
// TODO: 从区域服务获取区域名称
return "某区域";
}
/**
* 工单状态变更时,更新 Redis 中的活跃工单状态标记
* <p>
@@ -1047,4 +868,95 @@ public class CleanOrderEventListener {
log.warn("[CleanOrderEventListener] 记录完成业务日志失败: orderId={}", orderId, e);
}
}
/**
* 记录工单派单业务日志
*/
private void recordDispatchedLog(Long orderId, Long deviceId, OrderStateChangedEvent event) {
try {
OperatorTypeEnum operatorType = event.getOperatorType();
Long operatorId = event.getOperatorId();
String assigneeName = (String) event.getPayload().get("assigneeName");
String message;
if (event.getOldStatus() == WorkOrderStatusEnum.PAUSED) {
message = "工单从暂停恢复,重新派发";
} else if (operatorType == OperatorTypeEnum.ADMIN && operatorId != null) {
// 尝试从 event payload 获取操作人姓名
String opName = (String) event.getPayload().get("operatorName");
String opLabel = opName != null ? opName : "操作人";
String targetLabel = assigneeName != null ? assigneeName : "设备";
message = String.format("%s 将工单派发给 %s", opLabel, targetLabel);
} else {
message = assigneeName != null
? String.format("工单已派发给 %s", assigneeName)
: "工单已自动派发";
}
EventLogRecord.EventLogRecordBuilder builder = EventLogRecord.builder()
.module(LogModule.CLEAN)
.domain(EventDomain.DISPATCH)
.eventType(LogType.ORDER_DISPATCHED.getCode())
.message(message)
.targetId(orderId)
.targetType("order");
if (deviceId != null) {
builder.deviceId(deviceId);
}
if (operatorType == OperatorTypeEnum.ADMIN && operatorId != null) {
builder.personId(operatorId);
} else if (deviceId != null) {
builder.personId(deviceId);
}
eventLogRecorder.record(builder.build());
} catch (Exception e) {
log.warn("[CleanOrderEventListener] 记录派单业务日志失败: orderId={}", orderId, e);
}
}
/**
* 记录工单取消业务日志
*/
private void recordCancelledLog(Long orderId, Long deviceId, OrderStateChangedEvent event) {
try {
OperatorTypeEnum operatorType = event.getOperatorType();
Long operatorId = event.getOperatorId();
String remark = event.getRemark();
String message;
if (operatorType == OperatorTypeEnum.SYSTEM) {
message = "系统自动取消";
} else if (operatorType == OperatorTypeEnum.ADMIN) {
String opName = (String) event.getPayload().get("operatorName");
String opLabel = opName != null ? opName : "操作人";
message = opLabel + " 取消了工单";
} else {
message = "保洁工单已取消";
}
if (remark != null && !remark.isEmpty()) {
message += "" + remark + "";
}
EventLogRecord.EventLogRecordBuilder builder = EventLogRecord.builder()
.module(LogModule.CLEAN)
.domain(EventDomain.DISPATCH)
.eventType(LogType.ORDER_CANCELLED.getCode())
.message(message)
.targetId(orderId)
.targetType("order");
// personId: 管理员取消时为 operatorId系统取消时为设备/人员 ID
if (operatorType == OperatorTypeEnum.ADMIN && operatorId != null) {
builder.personId(operatorId);
} else if (deviceId != null) {
builder.personId(deviceId);
}
eventLogRecorder.record(builder.build());
} catch (Exception e) {
log.warn("[CleanOrderEventListener] 记录取消业务日志失败: orderId={}", orderId, e);
}
}
}

View File

@@ -15,10 +15,11 @@ import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.OperatorTypeEnum;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.environment.service.cleanorder.dto.CleanOrderAutoCreateReqDTO;
import com.viewsh.module.ops.enums.WorkOrderTypeEnum;
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.integration.listener.CleanOrderEventListener;
import com.viewsh.module.ops.environment.service.cleanorder.dto.CleanOrderAutoCreateReqDTO;
import com.viewsh.module.ops.environment.service.notification.CleanOrderNotificationService;
import com.viewsh.module.ops.infrastructure.area.AreaPathBuilder;
import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
@@ -77,7 +78,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
private OrderLifecycleManager orderLifecycleManager;
@Resource
private CleanOrderEventListener cleanOrderEventListener;
private CleanOrderNotificationService cleanOrderNotificationService;
@Resource
private ObjectMapper objectMapper;
@@ -92,13 +93,13 @@ public class CleanOrderServiceImpl implements CleanOrderService {
public Long createAutoCleanOrder(CleanOrderAutoCreateReqDTO createReq) {
// 1. 生成ID和编号
Long orderId = orderIdGenerator.generate();
String orderCode = orderCodeGenerator.generate("CLEAN");
String orderCode = orderCodeGenerator.generate(WorkOrderTypeEnum.CLEAN.getType());
// 2. 构建主表数据
OpsOrderDO.OpsOrderDOBuilder orderBuilder = OpsOrderDO.builder()
.id(orderId)
.orderCode(orderCode)
.orderType("CLEAN")
.orderType(WorkOrderTypeEnum.CLEAN.getType())
.title(createReq.getTitle())
.description(createReq.getDescription())
.priority(createReq.getPriority() != null ? createReq.getPriority() : PriorityEnum.P2.getPriority())
@@ -144,7 +145,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
// 5. 发布工单创建事件
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(orderId)
.orderType("CLEAN")
.orderType(WorkOrderTypeEnum.CLEAN.getType())
.orderCode(orderCode)
.title(createReq.getTitle())
.areaId(createReq.getAreaId())
@@ -244,7 +245,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
orderQueueService.rebuildWaitingTasksByUserId(queueDTO.getUserId(), order.getAreaId());
// 6. 发送优先级升级通知
cleanOrderEventListener.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode(), orderId);
cleanOrderNotificationService.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode(), orderId);
return true;
}
@@ -287,7 +288,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
// 6. 如果升级到 P0仅重算等待队列不再触发打断
if (newPriority == PriorityEnum.P0) {
orderQueueService.rebuildWaitingTasksByUserId(queueDTO.getUserId(), order.getAreaId());
cleanOrderEventListener.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode(), orderId);
cleanOrderNotificationService.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode(), orderId);
log.warn("客流升级到P0已重算等待队列: orderId={}", orderId);
}
}
@@ -384,17 +385,17 @@ public class CleanOrderServiceImpl implements CleanOrderService {
@Override
public void playVoiceForNewOrder(Long deviceId) {
cleanOrderEventListener.sendNewOrderNotification(deviceId, null);
cleanOrderNotificationService.sendNewOrderNotification(deviceId, null);
}
@Override
public void playVoiceForQueuedOrder(Long deviceId, int queueCount, Long orderId) {
cleanOrderEventListener.sendQueuedOrderNotification(deviceId, queueCount, orderId);
cleanOrderNotificationService.sendQueuedOrderNotification(deviceId, queueCount, orderId);
}
@Override
public void playVoiceForNextTask(Long deviceId, int queueCount, String nextTaskTitle) {
cleanOrderEventListener.sendNextTaskNotification(deviceId, queueCount, nextTaskTitle);
cleanOrderNotificationService.sendNextTaskNotification(deviceId, queueCount, nextTaskTitle);
}
// ==================== 作业时长计算 ====================