chore: 【ops】保洁工单基础Service

This commit is contained in:
lzh
2026-01-06 10:52:40 +08:00
parent 77b7bfb89d
commit c07f864f7e
2 changed files with 891 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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<OrderQueueDTO> 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<OpsCleanerStatusDO> 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<OrderQueueDTO> 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);
}
}
}