diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderService.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderService.java new file mode 100644 index 0000000..e30de72 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderService.java @@ -0,0 +1,189 @@ +package com.viewsh.module.ops.environment.service.cleanorder; + + +import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO; +import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderPauseReqDTO; +import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderResumeReqDTO; + +/** + * 保洁工单服务 + * + * 职责: + * 1. 自动创建保洁工单 + * 2. 队列推送管理(空闲推送,忙碌入队) + * 3. 保洁特有的状态转换(confirmOrder、信标触发) + * 4. 作业时长计算 + * 5. 保洁扩展信息管理 + * 6. 并发控制和去重合并播报 + * + * @author lzh + */ +public interface CleanOrderService { + + // ========== 工单创建与队列管理 ========== + + /** + * 自动创建保洁工单 + * 场景:定时任务、系统触发、IoT设备触发 + * + * @param createReq 创建请求 + * @return 工单ID + */ + Long createAutoCleanOrder(CleanOrderAutoCreateReqDTO createReq); + + /** + * 工单入队(不推送,状态:PENDING → QUEUED) + * 用于忙碌保洁员收到新工单时 + * + * @param orderId 工单ID + * @param cleanerId 保洁员ID + */ + void enqueueOrderOnly(Long orderId, Long cleanerId); + + /** + * 入队并立即推送(空闲保洁员,状态:PENDING → QUEUED → DISPATCHED) + * + * @param orderId 工单ID + * @param cleanerId 保洁员ID + */ + void enqueueAndDispatch(Long orderId, Long cleanerId); + + /** + * 推送工单到工牌(状态:QUEUED → DISPATCHED) + * 支持并发控制,防止重复推送 + * + * @param queueId 队列ID + */ + void dispatchToCleaner(Long queueId); + + // ========== 保洁特有的状态转换 ========== + + /** + * 保洁员按键确认工单 + * 状态转换:DISPATCHED → CONFIRMED + * 同时更新保洁员状态:IDLE → BUSY(关键:确认后才变为忙碌) + * + * @param orderId 工单ID + * @param cleanerId 保洁员ID + */ + void confirmOrder(Long orderId, Long cleanerId); + + /** + * 感知信标,开始作业 + * 状态转换:CONFIRMED → ARRIVED + * + * 注意:此方法由IoT信标事件触发,暂不实现,留接口 + * + * @param orderId 工单ID + * @param cleanerId 保洁员ID + * @param beaconId 信标ID + */ + void startWorkingOnBeacon(Long orderId, Long cleanerId, Long beaconId); + + /** + * 丢失信号,自动完成 + * 状态转换:ARRIVED → COMPLETED + * 完成后自动推送队列中的下一个任务 + * + * 注意:此方法由IoT信标事件触发,暂不实现,留接口 + * + * @param orderId 工单ID + * @param cleanerId 保洁员ID + * @param beaconId 信标ID + * @param lostSeconds 丢失信号时长(秒) + */ + void autoCompleteOnSignalLost(Long orderId, Long cleanerId, Long beaconId, Integer lostSeconds); + + // ========== 任务自动切换 ========== + + /** + * 任务完成后,自动推送队列中的下一个任务 + * + * @param completedOrderId 已完成的工单ID + * @param cleanerId 保洁员ID + */ + void autoDispatchNextOrder(Long completedOrderId, Long cleanerId); + + // ========== 语音播报(支持去重合并) ========== + + /** + * 语音播报:空闲保洁员收到新工单 + * + * @param cleanerId 保洁员ID + */ + void playVoiceForNewOrder(Long cleanerId); + + /** + * 语音播报:忙碌保洁员收到待办(支持去重合并) + * 例如:"新增3项待办,您共有5个待办工单" + * + * @param cleanerId 保洁员ID + * @param queueCount 当前待办数量 + */ + void playVoiceForQueuedOrder(Long cleanerId, int queueCount); + + /** + * 语音播报:任务完成后自动推送下一个 + * 例如:"待办工单总数2个,第一位待办工单xxx" + * + * @param cleanerId 保洁员ID + * @param queueCount 待办数量 + * @param nextTaskTitle 下一个任务标题 + */ + void playVoiceForNextTask(Long cleanerId, int queueCount, String nextTaskTitle); + + // ========== 暂停/恢复(调用通用服务)========== + + /** + * 暂停保洁工单(状态转换:ARRIVED -> PAUSED) + * 场景:P0紧急工单插队、临时离开 + * + * @param pauseReq 暂停请求 + */ + void pauseCleanOrder(CleanOrderPauseReqDTO pauseReq); + + // ========== 优先级管理 ========== + /** + * 升级工单优先级为P0并处理紧急插队 + * 场景:巡检员手动将普通工单升级为P0紧急任务 + * + * @param orderId 工单ID + * @param reason 升级原因 + * @return 是否成功 + */ + boolean upgradePriorityToP0(Long orderId, String reason); + + /** + * 恢复保洁工单(状态转换:PAUSED -> ARRIVED) + * + * @param resumeReq 恢复请求 + */ + void resumeCleanOrder(CleanOrderResumeReqDTO resumeReq); + + // ========== 作业时长计算 ========== + + /** + * 计算实际作业时长(秒) + * + * @param orderId 工单ID + * @return 作业时长(秒) + */ + Integer calculateActualDuration(Long orderId); + + /** + * 记录到岗时间 + * 场景:状态机监听器调用 + * + * @param orderId 工单ID + */ + void recordArrivedTime(Long orderId); + + /** + * 计算并更新作业时长 + * 场景:工单完成时,状态机监听器调用 + * + * @param orderId 工单ID + */ + void calculateDuration(Long orderId); + +} diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderServiceImpl.java new file mode 100644 index 0000000..2f01ab9 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanOrderServiceImpl.java @@ -0,0 +1,702 @@ +package com.viewsh.module.ops.environment.service.cleanorder; + +import com.viewsh.module.ops.api.dispatch.DispatchEngineService; +import com.viewsh.module.ops.api.queue.OrderQueueDTO; +import com.viewsh.module.ops.api.queue.OrderQueueService; +import com.viewsh.module.ops.dal.dataobject.cleaner.OpsCleanerStatusDO; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderCleanExtDO; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderCleanExtMapper; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; +import com.viewsh.module.ops.enums.CleanerStatusEnum; +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.dal.dataobject.CleanOrderAutoCreateReqDTO; +import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderPauseReqDTO; +import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderResumeReqDTO; +import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService; +import com.viewsh.module.ops.environment.service.dispatch.CleanerAreaPriorityStrategy; +import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastDeduplicationService; +import com.viewsh.module.ops.service.fsm.OrderStateMachine; +import com.viewsh.module.ops.service.order.OpsOrderService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 保洁工单扩展服务实现 + * + * @author lzh + */ +@Slf4j +@Service +public class CleanOrderServiceImpl implements CleanOrderService { + + @Resource + private OpsOrderService opsOrderService; + + @Resource + private OpsOrderMapper opsOrderMapper; + + @Resource + private OpsOrderCleanExtMapper cleanExtMapper; + + @Resource + private OrderQueueService orderQueueService; + + @Resource + private CleanerAreaPriorityStrategy cleanerAreaPriorityStrategy; + + @Resource + private CleanerStatusService cleanerStatusService; + + @Resource + private VoiceBroadcastDeduplicationService voiceBroadcastDeduplicationService; + + @Resource + private OrderStateMachine orderStateMachine; + + @Resource + private DispatchEngineService dispatchEngineService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createAutoCleanOrder(CleanOrderAutoCreateReqDTO createReq) { + // 1. 调用基础服务创建工单 + Long orderId = opsOrderService.createOrder(createReq); + + // 2. 创建保洁扩展信息 + OpsOrderCleanExtDO cleanExt = OpsOrderCleanExtDO.builder() + .opsOrderId(orderId) + .isAuto(1) // 自动工单 + .expectedDuration(createReq.getExpectedDuration()) + .cleaningType(createReq.getCleaningType()) + .difficultyLevel(createReq.getDifficultyLevel()) + .build(); + + cleanExtMapper.insert(cleanExt); + + log.info("创建自动保洁工单成功: orderId={}, expectedDuration={}分钟", + orderId, createReq.getExpectedDuration()); + + // 3. 自动分配保洁员并入队 + enqueueOrder(orderId, createReq.getAreaId(), PriorityEnum.fromPriority(createReq.getPriority())); + + return orderId; + } + + /** + * 工单入队(根据保洁员状态选择不同策略) + * 推荐保洁员、入队、派单、同步状态 + * + * @param orderId 工单ID + * @param areaId 区域ID + * @param priority 优先级 + */ + private void enqueueOrder(Long orderId, Long areaId, PriorityEnum priority) { + try { + // 1. 推荐保洁员 + Long recommendedCleanerId = cleanerAreaPriorityStrategy.recommendCleanerForNewOrder(areaId, priority); + + if (recommendedCleanerId == null) { + log.warn("未找到可用的保洁员,工单将无法自动派单: orderId={}, areaId={}", orderId, areaId); + // 工单保持在PENDING状态,等待后续处理 + return; + } + + // 2. 查询保洁员状态 + OpsCleanerStatusDO cleanerStatusDO = + cleanerStatusService.getStatus(recommendedCleanerId); + + if (cleanerStatusDO == null) { + log.warn("保洁员状态不存在,无法派单: cleanerId={}", recommendedCleanerId); + return; + } + + CleanerStatusEnum cleanerStatus = cleanerStatusDO.getStatusEnum(); + + // 3. P0 紧急任务特殊处理 + boolean isUrgent = priority == PriorityEnum.P0; + + if (cleanerStatus == CleanerStatusEnum.IDLE) { + // 空闲状态:立即推送 + log.info("保洁员空闲,立即推送工单: cleanerId={}, isUrgent={}", recommendedCleanerId, isUrgent); + enqueueAndDispatch(orderId, recommendedCleanerId, isUrgent); + } else if (cleanerStatus == CleanerStatusEnum.BUSY) { + if (isUrgent) { + // P0 紧急任务:使用派单引擎处理(可能打断当前任务) + log.warn("保洁员忙碌但为P0紧急任务,使用紧急派单: cleanerId={}", recommendedCleanerId); + enqueueAndDispatch(orderId, recommendedCleanerId, true); + } else { + // 忙碌状态:只入队 + log.info("保洁员忙碌,工单入队等待: cleanerId={}", recommendedCleanerId); + enqueueOrderOnly(orderId, recommendedCleanerId); + + // 语音提示待办数量 + int queueCount = getQueueCount(recommendedCleanerId); + playVoiceForQueuedOrder(recommendedCleanerId, queueCount); + } + } else { + log.warn("保洁员状态不可接单: cleanerId={}, status={}", + recommendedCleanerId, cleanerStatus); + } + + } catch (Exception e) { + log.error("工单入队或派单失败: orderId={}, areaId={}", orderId, areaId, e); + // TODO: 可以考虑回滚工单创建,或者标记为"入队失败"状态 + } + } + + // ========== 队列推送机制实现 ========== + + @Override + @Transactional(rollbackFor = Exception.class) + public void enqueueOrderOnly(Long orderId, Long cleanerId) { + log.info("工单入队(不推送): orderId={}, cleanerId={}", orderId, cleanerId); + + // 1. 查询工单 + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.error("工单不存在: orderId={}", orderId); + throw new IllegalArgumentException("工单不存在: " + orderId); + } + + // 2. 设置执行人 + order.setAssigneeId(cleanerId); + opsOrderMapper.updateById(order); + + // 3. 使用状态机转换状态:PENDING → QUEUED + orderStateMachine.transition( + order, + WorkOrderStatusEnum.QUEUED, + OperatorTypeEnum.SYSTEM, + cleanerId, + "工单入队,等待推送" + ); + + // 4. 创建队列记录(使用 OrderQueueService,等待后续推送) + Long queueId = orderQueueService.enqueue( + orderId, + cleanerId, + PriorityEnum.fromPriority(order.getPriority()), + null // queueIndex 自动计算 + ); + + log.info("工单已入队等待: orderId={}, cleanerId={}, queueId={}, status=QUEUED", + orderId, cleanerId, queueId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void enqueueAndDispatch(Long orderId, Long cleanerId) { + enqueueAndDispatch(orderId, cleanerId, false); + } + + /** + * 工单入队并立即推送(内部方法,支持紧急派单) + * + * @param orderId 工单ID + * @param cleanerId 保洁员ID + * @param isUrgent 是否为紧急任务(P0) + */ + @Transactional(rollbackFor = Exception.class) + public void enqueueAndDispatch(Long orderId, Long cleanerId, boolean isUrgent) { + log.info("工单入队并立即推送: orderId={}, cleanerId={}, isUrgent={}", orderId, cleanerId, isUrgent); + + // 1. 查询工单 + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.error("工单不存在: orderId={}", orderId); + throw new IllegalArgumentException("工单不存在: " + orderId); + } + + // 2. 更新工单状态:PENDING → QUEUED + order.setStatus(WorkOrderStatusEnum.QUEUED.getStatus()); + order.setAssigneeId(cleanerId); + opsOrderMapper.updateById(order); + + // 3. 创建队列记录 + Long queueId = orderQueueService.enqueue( + orderId, + cleanerId, + PriorityEnum.fromPriority(order.getPriority()), + null + ); + + // 4. 立即推送到工牌(根据是否紧急使用不同的派单策略) + if (isUrgent) { + // P0 紧急任务:使用派单引擎的紧急派单(可能打断当前任务) + boolean success = dispatchEngineService.urgentDispatch(queueId); + if (!success) { + log.warn("紧急派单失败,回退到普通派单: queueId={}", queueId); + dispatchToCleaner(queueId); + } + } else { + // 普通任务:正常派单 + dispatchToCleaner(queueId); + } + + // 5. 语音播报 + playVoiceForNewOrder(cleanerId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void dispatchToCleaner(Long queueId) { + log.info("推送工单到工牌: queueId={}", queueId); + + // 1. 查询队列记录 + OrderQueueDTO queueDTO = orderQueueService.getById(queueId); + if (queueDTO == null) { + log.error("队列记录不存在: queueId={}", queueId); + return; + } + + // 2. 并发控制:检查是否已有任务在推送中(状态为DISPATCHED) + boolean hasDispatchingTask = checkIfHasDispatchingTask(queueDTO.getUserId()); + if (hasDispatchingTask) { + log.warn("保洁员已有任务在推送中,跳过本次推送: queueId={}, cleanerId={}", + queueId, queueDTO.getUserId()); + return; + } + + // 3. 查询工单 + OpsOrderDO order = opsOrderMapper.selectById(queueDTO.getOpsOrderId()); + if (order == null) { + log.error("工单不存在: orderId={}", queueDTO.getOpsOrderId()); + return; + } + + // 4. 使用派单引擎进行自动派单(管理队列状态转换) + boolean dispatchSuccess = dispatchEngineService.autoDispatch(queueId); + + if (!dispatchSuccess) { + log.error("派单失败: queueId={}", queueId); + return; + } + + // 5. 使用状态机转换状态:QUEUED → DISPATCHED + orderStateMachine.transition( + order, + WorkOrderStatusEnum.DISPATCHED, + OperatorTypeEnum.SYSTEM, + queueDTO.getUserId(), + "推送工单" + ); + + log.info("工单已推送到工牌: queueId={}, orderId={}, cleanerId={}, status=DISPATCHED", + queueId, queueDTO.getOpsOrderId(), queueDTO.getUserId()); + + // TODO: 调用 IoT 服务推送工牌通知 + // iotDeviceService.pushNotification(queueDTO.getUserId(), + // "您有新的工单:" + order.getTitle()); + // iotDeviceService.vibrate(queueDTO.getUserId(), 1000); + } + + /** + * 检查保洁员是否已有任务在推送中(状态为DISPATCHED) + * 用于并发控制,防止重复推送 + */ + private boolean checkIfHasDispatchingTask(Long cleanerId) { + // 查询该保洁员是否有状态为 DISPATCHED 的队列记录 + List tasks = + orderQueueService.getTasksByUserId(cleanerId); + + return tasks.stream() + .anyMatch(task -> "DISPATCHED".equals(task.getQueueStatus())); + } + + /** + * 查询保洁员在队列中的待办工单数量 + */ + private int getQueueCount(Long cleanerId) { + Long count = orderQueueService.countByUserId(cleanerId); + return count != null ? count.intValue() : 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void pauseCleanOrder(CleanOrderPauseReqDTO pauseReq) { + // 1. 调用基础服务暂停工单 + opsOrderService.pauseOrder(pauseReq.getOrderId(), pauseReq.getCleanerId(), pauseReq.getPauseReason()); + + // 2. 同步更新队列状态:DISPATCHED → PAUSED + OrderQueueDTO queueDTO = orderQueueService.getByOpsOrderId(pauseReq.getOrderId()); + if (queueDTO != null) { + orderQueueService.pauseTask(queueDTO.getId()); + log.info("队列状态已更新为PAUSED: queueId={}, orderId={}", queueDTO.getId(), pauseReq.getOrderId()); + } + + // 3. 更新保洁扩展表:记录暂停开始时间 + OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(pauseReq.getOrderId()); + if (cleanExt != null) { + OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); + updateObj.setId(cleanExt.getId()); + updateObj.setPauseStartTime(LocalDateTime.now()); + cleanExtMapper.updateById(updateObj); + } + + log.info("暂停保洁工单成功: orderId={}, cleanerId={}, reason={}", + pauseReq.getOrderId(), pauseReq.getCleanerId(), pauseReq.getPauseReason()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void resumeCleanOrder(CleanOrderResumeReqDTO resumeReq) { + // 1. 调用基础服务恢复工单 + opsOrderService.resumeOrder(resumeReq.getOrderId(), resumeReq.getCleanerId()); + + // 2. 同步更新队列状态:PAUSED → DISPATCHED + OrderQueueDTO queueDTO = orderQueueService.getByOpsOrderId(resumeReq.getOrderId()); + if (queueDTO != null) { + orderQueueService.resumeTask(queueDTO.getId()); + log.info("队列状态已更新为DISPATCHED: queueId={}, orderId={}", queueDTO.getId(), resumeReq.getOrderId()); + } + + // 3. 更新保洁扩展表:计算暂停时长 + OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(resumeReq.getOrderId()); + if (cleanExt != null && cleanExt.getPauseStartTime() != null) { + long pauseSeconds = Duration.between(cleanExt.getPauseStartTime(), LocalDateTime.now()).getSeconds(); + + OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); + updateObj.setId(cleanExt.getId()); + updateObj.setPauseEndTime(LocalDateTime.now()); + updateObj.setTotalPauseSeconds((cleanExt.getTotalPauseSeconds() != null ? cleanExt.getTotalPauseSeconds() : 0) + (int) pauseSeconds); + cleanExtMapper.updateById(updateObj); + } + + log.info("恢复保洁工单成功: orderId={}, cleanerId={}", resumeReq.getOrderId(), resumeReq.getCleanerId()); + } + + // ========== 优先级管理 ========== + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean upgradePriorityToP0(Long orderId, String reason) { + log.warn("升级工单优先级为P0: orderId={}, reason={}", orderId, reason); + + // 1. 查询工单 + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.error("工单不存在: orderId={}", orderId); + return false; + } + + // 2. 检查当前优先级 + PriorityEnum currentPriority = PriorityEnum.fromPriority(order.getPriority()); + if (currentPriority == PriorityEnum.P0) { + log.info("工单已经是P0优先级,无需升级: orderId={}", orderId); + return true; + } + + // 3. 更新工单优先级 + order.setPriority(PriorityEnum.P0.getPriority()); + opsOrderMapper.updateById(order); + + // 4. 查询队列记录 + OrderQueueDTO queueDTO = orderQueueService.getByOpsOrderId(orderId); + if (queueDTO == null) { + log.warn("工单不在队列中,无需处理插队: orderId={}", orderId); + return true; + } + + // 5. 更新队列优先级 + boolean updated = orderQueueService.adjustPriority(queueDTO.getId(), PriorityEnum.P0, reason); + if (!updated) { + log.error("更新队列优先级失败: queueId={}", queueDTO.getId()); + return false; + } + + // 6. 使用派单引擎处理P0紧急插队(可能打断当前任务) + boolean interruptHandled = dispatchEngineService.handlePriorityInterrupt(orderId); + + log.info("P0优先级升级完成: orderId={}, interruptHandled={}", orderId, interruptHandled); + return interruptHandled; + } + + // ========== 保洁特有的状态转换 ========== + + @Override + @Transactional(rollbackFor = Exception.class) + public void confirmOrder(Long orderId, Long cleanerId) { + log.info("保洁员确认工单: orderId={}, cleanerId={}", orderId, cleanerId); + + // 1. 查询工单 + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.error("工单不存在: orderId={}", orderId); + throw new IllegalArgumentException("工单不存在: " + orderId); + } + + // 2. 校验工单状态 + com.viewsh.module.ops.enums.WorkOrderStatusEnum currentStatus = + com.viewsh.module.ops.enums.WorkOrderStatusEnum.fromStatus(order.getStatus()); + if (!currentStatus.canConfirm()) { + log.error("工单状态不允许确认: orderId={}, status={}", orderId, currentStatus); + throw new IllegalStateException("只有已推送状态的工单可以确认,当前状态: " + currentStatus.getDescription()); + } + + // 3. 校验保洁员(是否是分配给自己的工单) + if (!order.getAssigneeId().equals(cleanerId)) { + log.error("保洁员不能确认不属于自己的工单: orderId={}, assigneeId={}, cleanerId={}", + orderId, order.getAssigneeId(), cleanerId); + throw new IllegalStateException("不能确认不属于自己的工单"); + } + + // 4. 使用状态机转换状态:DISPATCHED → CONFIRMED + orderStateMachine.transition( + order, + WorkOrderStatusEnum.CONFIRMED, + OperatorTypeEnum.CLEANER, + cleanerId, + "确认工单" + ); + + // 5. 保洁员确认后转为忙碌状态(监听器中处理,这里显式调用保证逻辑完整) + cleanerStatusService.updateStatus(cleanerId, CleanerStatusEnum.BUSY, "已确认工单"); + + log.info("确认工单成功: orderId={}, cleanerId={}, status=CONFIRMED, cleanerStatus=BUSY", orderId, cleanerId); + + // TODO: 推送通知给巡检员(保洁员已确认) + // TODO: 记录操作日志 + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void startWorkingOnBeacon(Long orderId, Long cleanerId, Long beaconId) { + log.info("感知信标,开始作业: orderId={}, cleanerId={}, beaconId={}", orderId, cleanerId, beaconId); + + // 1. 查询工单 + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.error("工单不存在: orderId={}", orderId); + throw new IllegalArgumentException("工单不存在: " + orderId); + } + + // 2. 校验工单状态 + WorkOrderStatusEnum currentStatus = WorkOrderStatusEnum.valueOf(order.getStatus()); + if (!currentStatus.canStartWorking()) { + log.warn("工单状态不允许开始作业: orderId={}, status={}", orderId, currentStatus); + // 不抛异常,兼容某些场景 + return; + } + + // 3. 使用状态机转换状态:CONFIRMED → ARRIVED + orderStateMachine.transition( + order, + WorkOrderStatusEnum.ARRIVED, + OperatorTypeEnum.SYSTEM, + cleanerId, + "感知信标,开始作业" + ); + + // 4. 更新保洁员状态为忙碌 + cleanerStatusService.updateStatus(cleanerId, CleanerStatusEnum.BUSY, "开始作业"); + + // 5. 记录到岗时间到保洁扩展表(状态机监听器会处理,这里显式调用保证逻辑完整) + recordArrivedTime(orderId); + + log.info("开始作业成功: orderId={}, cleanerId={}, status=ARRIVED", orderId, cleanerId); + + // TODO: 推送通知给巡检员(保洁员已到达作业现场) + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void autoCompleteOnSignalLost(Long orderId, Long cleanerId, Long beaconId, Integer lostSeconds) { + log.info("丢失信号,自动完成工单: orderId={}, cleanerId={}, beaconId={}, lostSeconds={}秒", + orderId, cleanerId, beaconId, lostSeconds); + + // 1. 查询工单 + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.error("工单不存在: orderId={}", orderId); + throw new IllegalArgumentException("工单不存在: " + orderId); + } + + // 2. 校验工单状态 + WorkOrderStatusEnum currentStatus = WorkOrderStatusEnum.valueOf(order.getStatus()); + if (!currentStatus.canComplete()) { + log.warn("工单状态不允许完成: orderId={}, status={}", orderId, currentStatus); + return; + } + + // 3. 使用状态机转换状态:ARRIVED → COMPLETED + // 状态机会自动记录结束时间 + orderStateMachine.transition( + order, + WorkOrderStatusEnum.COMPLETED, + OperatorTypeEnum.SYSTEM, + cleanerId, + "丢失信号,自动完成" + ); + + // 4. 同步更新队列状态:DISPATCHED → COMPLETED + OrderQueueDTO queueDTO = orderQueueService.getByOpsOrderId(orderId); + if (queueDTO != null) { + orderQueueService.updateStatus(queueDTO.getId(), com.viewsh.module.ops.enums.OrderQueueStatusEnum.COMPLETED); + log.info("队列状态已更新为COMPLETED: queueId={}, orderId={}", queueDTO.getId(), orderId); + } + + // 5. 记录完成时间到保洁扩展表(状态机监听器会处理) + calculateDuration(orderId); + + log.info("工单自动完成: orderId={}, cleanerId={}, duration={}秒", + orderId, cleanerId, order.getCompletionSeconds()); + + // 6. 推送通知给巡检员(工单已完成,待验收) + notifyInspectorsOrderCompleted(orderId, cleanerId); + + // 7. 自动推送队列中的下一个任务(状态机监听器会处理,这里显式调用保证逻辑完整) + autoDispatchNextOrder(orderId, cleanerId); + } + + /** + * 推送通知给巡检员(工单已完成,待验收) + * 场景:保洁员完成任务后,通知巡检员进行验收 + * + * @param orderId 工单ID + * @param cleanerId 保洁员ID + */ + private void notifyInspectorsOrderCompleted(Long orderId, Long cleanerId) { + // TODO: 下一阶段实现 + // 1. 查询工单所属区域 + // 2. 查询该区域的巡检员列表 + // 3. 推送消息给巡检员(IoT工牌、App推送等) + // 4. 消息内容:保洁工单XXX已完成,请前往验收 + + log.info("推送验收通知给巡检员: orderId={}, cleanerId={}", orderId, cleanerId); + + // 示例代码(下一阶段实现): + // OpsOrderDO order = opsOrderMapper.selectById(orderId); + // List inspectors = cleanerStatusService.listInspectorsByArea(order.getAreaId()); + // for (OpsCleanerStatusDO inspector : inspectors) { + // iotDeviceService.pushNotification(inspector.getDeviceId(), + // String.format("保洁工单%s已完成,请前往%s验收", order.getOrderCode(), order.getLocation())); + // } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void autoDispatchNextOrder(Long completedOrderId, Long cleanerId) { + log.info("推送下一个任务: completedOrderId={}, cleanerId={}", completedOrderId, cleanerId); + + // 1. 查询队列中等待的任务 + List queuedTasks = orderQueueService.getWaitingTasksByUserId(cleanerId); + + if (queuedTasks.isEmpty()) { + // 没有待办任务,保洁员变空闲 + cleanerStatusService.updateStatus(cleanerId, CleanerStatusEnum.IDLE, "所有任务已完成"); + cleanerStatusService.clearCurrentWorkOrder(cleanerId); + log.info("保洁员所有任务已完成,转为空闲状态: cleanerId={}", cleanerId); + return; + } + + // 2. 推送队列中的第一个任务 + OrderQueueDTO nextTask = queuedTasks.get(0); + dispatchToCleaner(nextTask.getId()); + + // 3. 语音播报 + int queueCount = queuedTasks.size(); + OpsOrderDO nextOrder = opsOrderMapper.selectById(nextTask.getOpsOrderId()); + String nextTaskTitle = nextOrder != null ? nextOrder.getTitle() : "未知任务"; + playVoiceForNextTask(cleanerId, queueCount, nextTaskTitle); + + log.info("已自动推送下一个任务: taskId={}, orderId={}, queueCount={}", + nextTask.getId(), nextTask.getOpsOrderId(), queueCount); + } + + // ========== 语音播报方法(支持去重合并) ========== + + @Override + public void playVoiceForNewOrder(Long cleanerId) { + log.info("语音播报:空闲保洁员收到新工单: cleanerId={}", cleanerId); + + // 空闲保洁员收到新工单,立即播报(不合并) + // TODO: 调用 IoT 设备服务进行语音播报 + // iotDeviceService.playVoice(cleanerId, "您有1条新的待办工单"); + + // TODO: 调用 IoT 设备服务进行震动提醒 + // iotDeviceService.vibrate(cleanerId, 1000); + } + + @Override + public void playVoiceForQueuedOrder(Long cleanerId, int queueCount) { + log.info("语音播报:忙碌保洁员收到待办(合并播报): cleanerId={}, queueCount={}", cleanerId, queueCount); + + // 忙碌保洁员收到新工单,使用去重服务合并播报 + voiceBroadcastDeduplicationService.recordAndBroadcast(cleanerId, 1, false); + } + + @Override + public void playVoiceForNextTask(Long cleanerId, int queueCount, String nextTaskTitle) { + log.info("语音播报:任务完成后自动推送下一个: cleanerId={}, queueCount={}, taskTitle={}", + cleanerId, queueCount, nextTaskTitle); + + // 任务完成后自动推送下一个,立即播报(不合并) + // TODO: 调用 IoT 设备服务进行语音播报 + String message = String.format("待办工单总数%d个,第一位待办工单%s", queueCount, nextTaskTitle); + // iotDeviceService.playVoice(cleanerId, message); + + // TODO: 震动提醒 + // iotDeviceService.vibrate(cleanerId, 1000); + } + + // ========== 作业时长计算 ========== + + @Override + public Integer calculateActualDuration(Long orderId) { + OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId); + if (cleanExt == null || cleanExt.getArrivedTime() == null || cleanExt.getCompletedTime() == null) { + return 0; + } + + // 计算实际作业时长(秒) + long actualSeconds = Duration.between(cleanExt.getArrivedTime(), cleanExt.getCompletedTime()).getSeconds(); + + // 减去累计暂停时长 + int totalPauseSeconds = cleanExt.getTotalPauseSeconds() != null ? cleanExt.getTotalPauseSeconds() : 0; + long workSeconds = actualSeconds - totalPauseSeconds; + + return (int) Math.max(workSeconds, 0); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void recordArrivedTime(Long orderId) { + // 更新保洁扩展表:记录到岗时间 + OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId); + if (cleanExt != null) { + OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); + updateObj.setId(cleanExt.getId()); + updateObj.setArrivedTime(LocalDateTime.now()); + cleanExtMapper.updateById(updateObj); + + log.info("记录到岗时间: orderId={}", orderId); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void calculateDuration(Long orderId) { + // 更新保洁扩展表:记录完成时间 + OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId); + if (cleanExt != null) { + OpsOrderCleanExtDO updateObj = new OpsOrderCleanExtDO(); + updateObj.setId(cleanExt.getId()); + updateObj.setCompletedTime(LocalDateTime.now()); + cleanExtMapper.updateById(updateObj); + + // 计算实际作业时长 + Integer actualDuration = calculateActualDuration(orderId); + log.info("工单完成: orderId={}, actualDuration={}秒", orderId, actualDuration); + } + } + +}