diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderArriveEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderArriveEventHandler.java index dbdc49c..068da7b 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderArriveEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderArriveEventHandler.java @@ -8,6 +8,9 @@ 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.environment.integration.dto.CleanOrderArriveEventDTO; +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 jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; @@ -73,6 +76,9 @@ public class CleanOrderArriveEventHandler implements RocketMQListener { @Resource private OrderLifecycleManager orderLifecycleManager; + @Resource + private EventLogRecorder eventLogRecorder; + @Override public void onMessage(String message) { try { @@ -146,7 +152,10 @@ public class CleanOrderArriveEventHandler implements RocketMQListener { // 5. 通过生命周期管理器执行状态转换(DISPATCHED/CONFIRMED -> ARRIVED) orderLifecycleManager.transition(request); - // 6. 更新 Redis 缓存(设备当前工单) + // 6. 记录业务日志 + recordOrderArrivedLog(event, request); + + // 7. 更新 Redis 缓存(设备当前工单) cacheDeviceCurrentOrder(event); log.info("[CleanOrderArriveEventHandler] 工单到岗成功: eventId={}, orderId={}, deviceId={}", @@ -190,4 +199,36 @@ public class CleanOrderArriveEventHandler implements RocketMQListener { log.error("[CleanOrderArriveEventHandler] 设备工单缓存更新失败: deviceId={}", event.getDeviceId(), e); } } + + /** + * 记录工单到岗业务日志 + */ + private void recordOrderArrivedLog(CleanOrderArriveEventDTO event, OrderTransitionRequest request) { + try { + // 构建扩展信息 + Map extra = new HashMap<>(); + extra.put("eventId", event.getEventId()); + extra.put("triggerSource", event.getTriggerSource()); + extra.put("areaId", event.getAreaId()); + if (event.getTriggerData() != null) { + extra.putAll(event.getTriggerData()); + } + + // 记录日志 + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(EventDomain.BEACON) + .eventType("ORDER_ARRIVED") + .message(String.format("蓝牙信标自动到岗确认 [设备:%s, 区域:%d]", + event.getDeviceKey(), event.getAreaId())) + .targetId(event.getOrderId()) + .targetType("order") + .deviceId(event.getDeviceId()) + .payload(extra) + .build()); + + } catch (Exception e) { + log.warn("[CleanOrderArriveEventHandler] 记录业务日志失败: orderId={}", event.getOrderId(), e); + } + } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCompleteEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCompleteEventHandler.java index 16fc7ca..904a0c3 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCompleteEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCompleteEventHandler.java @@ -9,6 +9,9 @@ import com.viewsh.module.ops.enums.OperatorTypeEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.environment.integration.dto.CleanOrderCompleteEventDTO; import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService; +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 jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; @@ -72,6 +75,9 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener @Resource private CleanOrderService cleanOrderService; + @Resource + private EventLogRecorder eventLogRecorder; + @Override public void onMessage(String message) { try { @@ -143,10 +149,13 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener // 5. 通过生命周期管理器执行状态转换(ARRIVED -> COMPLETED) orderLifecycleManager.completeOrder(event.getOrderId(), null, remark); - // 6. 清除 Redis 缓存(设备当前工单) + // 6. 记录业务日志 + recordOrderCompletedLog(event, order, remark); + + // 7. 清除 Redis 缓存(设备当前工单) clearDeviceCurrentOrder(event.getDeviceId()); - // 7. 自动调度下一个任务(优先恢复被中断的任务) + // 8. 自动调度下一个任务(优先恢复被中断的任务) if (order.getAssigneeId() != null) { cleanOrderService.autoDispatchNextOrder(event.getOrderId(), order.getAssigneeId()); } @@ -187,4 +196,49 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener log.error("[CleanOrderCompleteEventHandler] 设备工单缓存清除失败: deviceId={}", deviceId, e); } } + + /** + * 记录工单完成业务日志 + */ + private void recordOrderCompletedLog(CleanOrderCompleteEventDTO event, OpsOrderDO order, String remark) { + try { + // 构建扩展信息 + Map extra = new HashMap<>(); + extra.put("eventId", event.getEventId()); + extra.put("triggerSource", event.getTriggerSource()); + extra.put("areaId", event.getAreaId()); + extra.put("completionReason", event.getTriggerData() != null ? + event.getTriggerData().get("completionReason") : "SIGNAL_LOSS_TIMEOUT"); + if (event.getTriggerData() != null) { + extra.putAll(event.getTriggerData()); + } + + // 计算作业时长(分钟) + String durationInfo = ""; + if (event.getTriggerData() != null && event.getTriggerData().containsKey("durationMs")) { + Object durationMs = event.getTriggerData().get("durationMs"); + if (durationMs != null) { + long durationMinutes = ((Number) durationMs).longValue() / 60000; + durationInfo = String.format(",作业时长: %d分钟", durationMinutes); + extra.put("durationMinutes", durationMinutes); + } + } + + // 记录日志 + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(EventDomain.BEACON) + .eventType("ORDER_COMPLETED") + .message("信号丢失超时自动完成 [设备:" + event.getDeviceKey() + durationInfo + "]") + .targetId(event.getOrderId()) + .targetType("order") + .deviceId(event.getDeviceId()) + .personId(order.getAssigneeId()) + .payload(extra) + .build()); + + } catch (Exception e) { + log.warn("[CleanOrderCompleteEventHandler] 记录业务日志失败: orderId={}", event.getOrderId(), e); + } + } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java index 5ffcdf6..c09a6b2 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/consumer/CleanOrderCreateEventHandler.java @@ -7,6 +7,10 @@ import com.viewsh.module.ops.enums.PriorityEnum; import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO; import com.viewsh.module.ops.environment.integration.dto.CleanOrderCreateEventDTO; import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService; +import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain; +import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord; +import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; @@ -15,6 +19,8 @@ import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -60,6 +66,9 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { @Resource private IotDeviceControlApi iotDeviceControlApi; + @Resource + private EventLogRecorder eventLogRecorder; + @Override public void onMessage(String message) { try { @@ -117,7 +126,10 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { // 2. 创建工单(同时创建主表+扩展表) Long orderId = cleanOrderService.createAutoCleanOrder(createReq); - // 3. 如果是客流触发的工单,重置客流计数器基准值 + // 3. 记录业务日志 + recordOrderCreatedLog(event, orderId, createReq); + + // 4. 如果是客流触发的工单,重置客流计数器基准值 // TODO: 需要优化这个工单是否创建成功,才重置 if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) { resetTrafficCounter(event, orderId); @@ -127,6 +139,80 @@ public class CleanOrderCreateEventHandler implements RocketMQListener { event.getEventId(), orderId, event.getAreaId()); } + /** + * 记录工单创建业务日志 + */ + private void recordOrderCreatedLog(CleanOrderCreateEventDTO event, Long orderId, CleanOrderAutoCreateReqDTO createReq) { + try { + // 确定事件域和类型 + EventDomain domain = determineDomain(event.getTriggerSource()); + String eventType = "ORDER_CREATED"; + + // 构建扩展信息 + Map extra = new HashMap<>(); + extra.put("eventId", event.getEventId()); + extra.put("triggerSource", event.getTriggerSource()); + extra.put("areaId", event.getAreaId()); + extra.put("priority", createReq.getPriority()); + extra.put("expectedDuration", createReq.getExpectedDuration()); + if (event.getTriggerData() != null) { + extra.putAll(event.getTriggerData()); + } + + // 记录日志 + eventLogRecorder.info("clean", domain, eventType, + buildLogMessage(event, createReq), + orderId, + event.getTriggerDeviceId(), + null); + + // 添加扩展信息 + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(domain) + .eventType(eventType + "_DETAIL") + .message("工单创建详细数据") + .targetId(orderId) + .targetType("order") + .deviceId(event.getTriggerDeviceId()) + .level(EventLevel.INFO) + .payload(extra) + .build()); + + } catch (Exception e) { + log.warn("[CleanOrderCreateEventHandler] 记录业务日志失败: orderId={}", orderId, e); + } + } + + /** + * 确定事件域 + */ + private EventDomain determineDomain(String triggerSource) { + if ("IOT_TRAFFIC".equals(triggerSource)) { + return EventDomain.TRAFFIC; + } else if ("IOT_BEACON".equals(triggerSource)) { + return EventDomain.BEACON; + } else if ("IOT_SIGNAL_LOSS".equals(triggerSource)) { + return EventDomain.BEACON; + } + return EventDomain.SYSTEM; + } + + /** + * 构建日志消息 + */ + private String buildLogMessage(CleanOrderCreateEventDTO event, CleanOrderAutoCreateReqDTO createReq) { + if ("IOT_TRAFFIC".equals(event.getTriggerSource())) { + return String.format("客流阈值触发工单创建 [设备:%s, 区域:%d]", + event.getTriggerDeviceKey(), event.getAreaId()); + } else if ("IOT_BEACON".equals(event.getTriggerSource())) { + return String.format("信标检测触发工单创建 [设备:%s, 区域:%d]", + event.getTriggerDeviceKey(), event.getAreaId()); + } + return String.format("IoT设备触发工单创建 [设备:%s, 来源:%s]", + event.getTriggerDeviceKey(), event.getTriggerSource()); + } + /** * 重置客流计数器基准值 *

diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/BadgeDispatchTestConfig.java b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/BadgeDispatchTestConfig.java index 1d2396f..1c25d9b 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/BadgeDispatchTestConfig.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/test/BadgeDispatchTestConfig.java @@ -1,26 +1,22 @@ package com.viewsh.module.ops.environment.test; import com.fasterxml.jackson.databind.ObjectMapper; -import com.viewsh.module.ops.api.queue.OrderQueueDTO; import com.viewsh.module.ops.api.queue.OrderQueueService; import com.viewsh.module.ops.core.dispatch.DispatchEngine; import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService; -import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusServiceImpl; import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService; import com.viewsh.module.ops.environment.service.dispatch.BadgeDeviceAreaAssignStrategy; import com.viewsh.module.ops.environment.service.dispatch.BadgeDeviceScheduleStrategy; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.Collections; import java.util.HashSet; -import java.util.List; -import java.util.Set; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManagerImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManagerImpl.java index d6c349a..c78fd1d 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManagerImpl.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/lifecycle/OrderLifecycleManagerImpl.java @@ -16,6 +16,9 @@ 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.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.ops.service.fsm.OrderStateMachine; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; @@ -23,6 +26,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; /** @@ -66,6 +71,9 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { @Resource private EventPublishHandler eventPublishHandler; + @Resource + private EventLogRecorder eventLogRecorder; + /** * 责任链处理器 */ @@ -169,6 +177,9 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { if (!result.isSuccess()) { throw new IllegalStateException("暂停工单失败: " + result.getMessage()); } + + // 记录业务日志 + recordStatusChangeLog(orderId, result, "ORDER_PAUSED", "工单暂停"); } @Override @@ -191,6 +202,9 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { if (!result.isSuccess()) { throw new IllegalStateException("恢复工单失败: " + result.getMessage()); } + + // 记录业务日志 + recordStatusChangeLog(orderId, result, "ORDER_RESUMED", "工单恢复"); } // ==================== 打断/恢复 ==================== @@ -217,6 +231,24 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { if (!result.isSuccess()) { throw new IllegalStateException("打断工单失败: " + result.getMessage()); } + + // 记录业务日志 + Map extra = new HashMap<>(); + extra.put("urgentOrderId", urgentOrderId); + extra.put("interruptReason", "P0任务插队"); + + OpsOrderDO order = opsOrderMapper.selectById(orderId); + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(EventDomain.DISPATCH) + .eventType("ORDER_INTERRUPTED") + .message("工单被P0紧急任务打断") + .targetId(orderId) + .targetType("order") + .deviceId(order != null ? order.getAssigneeDeviceId() : null) + .personId(order != null ? order.getAssigneeId() : null) + .payload(extra) + .build()); } @Override @@ -252,6 +284,12 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { if (!result.isSuccess()) { throw new IllegalStateException("完成工单失败: " + result.getMessage()); } + + // 注意:IoT 触发的自动完成在 CleanOrderCompleteEventHandler 中记录日志 + // 这里只记录手动完成的日志(operatorId 不为空的情况) + if (operatorId != null) { + recordStatusChangeLog(orderId, result, "ORDER_COMPLETED_MANUAL", "工单手动完成"); + } } @Override @@ -260,6 +298,9 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { log.info("开始取消工单: orderId={}, operatorId={}, operatorType={}, reason={}", orderId, operatorId, operatorType, reason); + // 查询工单信息(日志记录用) + OpsOrderDO order = opsOrderMapper.selectById(orderId); + // 构建请求 OrderTransitionRequest request = OrderTransitionRequest.builder() .orderId(orderId) @@ -275,6 +316,23 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { if (!result.isSuccess()) { throw new IllegalStateException("取消工单失败: " + result.getMessage()); } + + // 记录业务日志 + Map extra = new HashMap<>(); + extra.put("cancelReason", reason); + extra.put("operatorType", operatorType != null ? operatorType.getType() : "SYSTEM"); + + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(EventDomain.SYSTEM) + .eventType("ORDER_CANCELLED") + .message("工单已取消: " + reason) + .targetId(orderId) + .targetType("order") + .deviceId(order != null ? order.getAssigneeDeviceId() : null) + .personId(order != null ? order.getAssigneeId() : null) + .payload(extra) + .build()); } // ==================== 查询方法 ==================== @@ -337,4 +395,42 @@ public class OrderLifecycleManagerImpl implements OrderLifecycleManager { || WorkOrderStatusEnum.CONFIRMED == status || WorkOrderStatusEnum.ARRIVED == status; } + + /** + * 记录状态变更业务日志 + * + * @param orderId 工单ID + * @param result 状态转换结果 + * @param eventType 事件类型 + * @param message 日志消息 + */ + private void recordStatusChangeLog(Long orderId, OrderTransitionResult result, + String eventType, String message) { + try { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + return; + } + + Map extra = new HashMap<>(); + extra.put("oldStatus", result.getOldStatus() != null ? result.getOldStatus().getStatus() : null); + extra.put("newStatus", result.getNewStatus() != null ? result.getNewStatus().getStatus() : null); + + eventLogRecorder.record(EventLogRecord.builder() + .module("clean") + .domain(EventDomain.DISPATCH) + .eventType(eventType) + .message(message) + .targetId(orderId) + .targetType("order") + .deviceId(order.getAssigneeDeviceId()) + .personId(order.getAssigneeId()) + .payload(extra) + .build()); + + } catch (Exception e) { + log.warn("[OrderLifecycleManager] 记录状态变更日志失败: orderId={}, eventType={}", + orderId, eventType, e); + } + } }