feat(ops): refactor-order-operations

This commit is contained in:
lzh
2026-01-19 13:32:23 +08:00
parent 5419a949d4
commit 568d37a0be
31 changed files with 2806 additions and 1456 deletions

View File

@@ -30,4 +30,18 @@ public class CleanOrderAutoCreateReqDTO extends OpsOrderCreateReqDTO {
@Min(value = 1, message = "难度等级必须大于0")
private Integer difficultyLevel;
// ==================== 集成字段IoT触发相关====================
@Schema(description = "触发来源IOT_TRAFFIC=客流阈值/IOT_BEACON=蓝牙信标/IOT_SIGNAL_LOSS=信号丢失超时)")
private String triggerSource;
@Schema(description = "触发规则ID关联 ops_area_device_relation.id")
private Long triggerRuleId;
@Schema(description = "触发设备ID关联 iot_device.id")
private Long triggerDeviceId;
@Schema(description = "触发设备Key冗余便于查询")
private String triggerDeviceKey;
}

View File

@@ -3,11 +3,10 @@ package com.viewsh.module.ops.environment.integration.consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
import com.viewsh.module.iot.api.device.dto.ResetTrafficCounterReqDTO;
import com.viewsh.module.ops.dal.dataobject.dto.OpsOrderCreateReqDTO;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.enums.SourceTypeEnum;
import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO;
import com.viewsh.module.ops.environment.integration.dto.CleanOrderCreateEventDTO;
import com.viewsh.module.ops.service.order.OpsOrderService;
import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
@@ -56,7 +55,7 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
private StringRedisTemplate stringRedisTemplate;
@Resource
private OpsOrderService opsOrderService;
private CleanOrderService cleanOrderService;
@Resource
private IotDeviceControlApi iotDeviceControlApi;
@@ -93,28 +92,33 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
log.info("[CleanOrderCreateEventHandler] 收到工单创建事件: eventId={}, areaId={}, triggerSource={}",
event.getEventId(), event.getAreaId(), event.getTriggerSource());
// 1. 构建创建请求
OpsOrderCreateReqDTO createReq = new OpsOrderCreateReqDTO();
createReq.setOrderType(event.getOrderType());
createReq.setSourceType(SourceTypeEnum.TRAFFIC.getType()); // 系统触发
// 1. 构建创建请求(使用 CleanOrderAutoCreateReqDTO
CleanOrderAutoCreateReqDTO createReq = new CleanOrderAutoCreateReqDTO();
createReq.setOrderType("CLEAN");
createReq.setSourceType("TRAFFIC"); // 系统触发
createReq.setTitle(generateOrderTitle(event));
createReq.setDescription(generateOrderDescription(event));
createReq.setPriority(PriorityEnum.fromPriority(event.getPriority() != null ?
Integer.parseInt(event.getPriority()) : 2).getPriority());
createReq.setAreaId(event.getAreaId());
// location 字段由 areaId 自动关联,不需要在事件中传递
// 2. 创建工单
Long orderId = opsOrderService.createOrder(createReq);
// 扩展字段
createReq.setExpectedDuration(calculateExpectedDuration(event));
createReq.setCleaningType("ROUTINE"); // 可根据triggerSource动态设置
createReq.setDifficultyLevel(3);
// 3. 更新工单的触发信息(集成字段
opsOrderService.updateIntegrationFields(
orderId,
event.getTriggerSource(),
event.getTriggerDeviceId(),
event.getTriggerDeviceKey()
);
// IoT集成字段
createReq.setTriggerSource(event.getTriggerSource());
createReq.setTriggerRuleId(extractRuleId(event));
createReq.setTriggerDeviceId(event.getTriggerDeviceId());
createReq.setTriggerDeviceKey(event.getTriggerDeviceKey());
// 4. 如果是客流触发的工单,重置客流计数器基准值
// 2. 创建工单(同时创建主表+扩展表)
Long orderId = cleanOrderService.createAutoCleanOrder(createReq);
// 3. 如果是客流触发的工单,重置客流计数器基准值
// TODO: 需要优化这个工单是否创建成功,才重置
if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) {
resetTrafficCounter(event, orderId);
}
@@ -181,6 +185,8 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
return "客流阈值触发保洁";
} else if ("IOT_BEACON".equals(event.getTriggerSource())) {
return "信标检测触发保洁";
} else if ("IOT_SIGNAL_LOSS".equals(event.getTriggerSource())) {
return "离线超时触发保洁";
} else {
return "IoT设备触发保洁";
}
@@ -205,4 +211,39 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
return desc.toString();
}
/**
* 计算预计作业时长(分钟)
*/
private Integer calculateExpectedDuration(CleanOrderCreateEventDTO event) {
// 根据触发来源和客流数据估算时长
if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) {
Object actualCountObj = event.getTriggerData().get("actualCount");
if (actualCountObj != null) {
Long actualCount = ((Number) actualCountObj).longValue();
// 客流越大,预计耗时越长
if (actualCount > 50) {
return 45; // 高客流
} else if (actualCount > 20) {
return 30; // 中客流
} else {
return 20; // 低客流
}
}
}
return 30; // 默认30分钟
}
/**
* 从事件数据中提取规则ID
*/
private Long extractRuleId(CleanOrderCreateEventDTO event) {
if (event.getTriggerData() != null && event.getTriggerData().containsKey("ruleId")) {
Object ruleIdObj = event.getTriggerData().get("ruleId");
if (ruleIdObj instanceof Number) {
return ((Number) ruleIdObj).longValue();
}
}
return null;
}
}

View File

@@ -1,152 +0,0 @@
package com.viewsh.module.ops.environment.integration.consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viewsh.module.ops.environment.integration.dto.DeviceEventOccurredEventDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.springframework.stereotype.Component;
/**
* 设备事件上报消费者
* <p>
* 订阅 IoT 模块发布的设备事件上报SOS、按键等
* <p>
* RocketMQ 配置:
* - Topic: integration.device.event
* - Tag: productKey可配置如 cleaner_badge_v1
*
* @author lzh
*/
@Slf4j
@Component
@org.apache.rocketmq.spring.annotation.RocketMQMessageListener(
topic = "integration.device.event",
consumerGroup = "ops-device-event-group",
consumeMode = ConsumeMode.CONCURRENTLY,
selectorExpression = "*"
)
public class DeviceEventEventHandler implements org.apache.rocketmq.spring.core.RocketMQListener<String> {
@Resource
private ObjectMapper objectMapper;
@Resource
private IntegrationEventDeduplicationService deduplicationService;
@Override
public void onMessage(String message) {
try {
// 1. JSON 反序列化
DeviceEventOccurredEventDTO event = objectMapper.readValue(message, DeviceEventOccurredEventDTO.class);
// 2. 幂等性检查
if (!deduplicationService.tryConsume(event.getEventId())) {
log.debug("[DeviceEventEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId());
return;
}
// 3. 业务处理
handleEventOccurred(event);
} catch (Exception e) {
log.error("[DeviceEventEventHandler] 消息处理失败: message={}", message, e);
throw new RuntimeException("设备事件上报处理失败", e);
}
}
/**
* 处理设备事件
*/
private void handleEventOccurred(DeviceEventOccurredEventDTO event) {
Long deviceId = event.getDeviceId();
String eventIdentifier = event.getEventIdentifier();
log.info("[DeviceEventEventHandler] 设备事件: deviceId={}, event={}, productKey={}",
deviceId, eventIdentifier, event.getProductKey());
// 根据事件类型分发处理
switch (eventIdentifier) {
case "sos":
handleSosEvent(event);
break;
case "button_click":
handleButtonClick(event);
break;
case "fall_detected":
handleFallDetected(event);
break;
case "confirm_order":
handleConfirmOrder(event);
break;
case "complete_order":
handleCompleteOrder(event);
break;
case "pause_order":
handlePauseOrder(event);
break;
default:
log.debug("[DeviceEventEventHandler] 未知事件类型: {}", eventIdentifier);
break;
}
log.debug("[DeviceEventEventHandler] 处理完成: deviceId={}, event={}", deviceId, eventIdentifier);
}
/**
* 处理 SOS 告警事件
*/
private void handleSosEvent(DeviceEventOccurredEventDTO event) {
log.warn("[DeviceEventEventHandler] SOS 告警: deviceId={}, productKey={}",
event.getDeviceId(), event.getProductKey());
// TODO: 触发紧急工单或告警
// 1. 查找设备关联的保洁员
// 2. 发送告警通知
// 3. 创建紧急工单
}
/**
* 处理按键点击事件
*/
private void handleButtonClick(DeviceEventOccurredEventDTO event) {
log.info("[DeviceEventEventHandler] 按键点击: deviceId={}", event.getDeviceId());
}
/**
* 处理跌倒检测事件
*/
private void handleFallDetected(DeviceEventOccurredEventDTO event) {
log.warn("[DeviceEventEventHandler] 跌倒检测: deviceId={}", event.getDeviceId());
// TODO: 触发紧急告警
}
/**
* 处理工单确认事件
*/
private void handleConfirmOrder(DeviceEventOccurredEventDTO event) {
log.info("[DeviceEventEventHandler] 工单确认: deviceId={}", event.getDeviceId());
// TODO: 更新工单状态为已确认
}
/**
* 处理工单完成事件
*/
private void handleCompleteOrder(DeviceEventOccurredEventDTO event) {
log.info("[DeviceEventEventHandler] 工单完成: deviceId={}", event.getDeviceId());
// TODO: 更新工单状态为已完成
}
/**
* 处理工单暂停事件
*/
private void handlePauseOrder(DeviceEventOccurredEventDTO event) {
log.info("[DeviceEventEventHandler] 工单暂停: deviceId={}", event.getDeviceId());
// TODO: 更新工单状态为已暂停
}
}

View File

@@ -1,117 +0,0 @@
package com.viewsh.module.ops.environment.integration.consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viewsh.module.ops.environment.integration.dto.DevicePropertyChangedEventDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
* 设备属性变更事件消费者
* <p>
* 订阅 IoT 模块发布的设备属性变更事件
* <p>
* RocketMQ 配置:
* - Topic: integration.device.property
* - Tag: productKey可配置如 cleaner_badge_v1
*
* @author lzh
*/
@Slf4j
@Component
@org.apache.rocketmq.spring.annotation.RocketMQMessageListener(
topic = "integration.device.property",
consumerGroup = "ops-device-property-group",
consumeMode = ConsumeMode.CONCURRENTLY,
selectorExpression = "*"
)
public class DevicePropertyEventHandler implements RocketMQListener<String> {
@Resource
private ObjectMapper objectMapper;
@Resource
private IntegrationEventDeduplicationService deduplicationService;
@Override
public void onMessage(String message) {
try {
// 1. JSON 反序列化
DevicePropertyChangedEventDTO event = objectMapper.readValue(message, DevicePropertyChangedEventDTO.class);
// 2. 幂等性检查
if (!deduplicationService.tryConsume(event.getEventId())) {
log.debug("[DevicePropertyEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId());
return;
}
// 3. 业务处理
handlePropertyChanged(event);
} catch (Exception e) {
log.error("[DevicePropertyEventHandler] 消息处理失败: message={}", message, e);
throw new RuntimeException("设备属性变更事件处理失败", e);
}
}
/**
* 处理设备属性变更
*/
private void handlePropertyChanged(DevicePropertyChangedEventDTO event) {
Long deviceId = event.getDeviceId();
var properties = event.getProperties();
log.info("[DevicePropertyEventHandler] 设备属性变更: deviceId={}, properties={}, productKey={}",
deviceId, properties.keySet(), event.getProductKey());
// 处理特定属性变更
if (properties.containsKey("battery")) {
handleBatteryChange(deviceId, properties.get("battery"));
}
if (properties.containsKey("signal")) {
handleSignalChange(deviceId, properties.get("signal"));
}
// 客流传感器数据检查
if (properties.containsKey("people_count")) {
handlePeopleCountChange(deviceId, properties.get("people_count"));
}
log.debug("[DevicePropertyEventHandler] 处理完成: deviceId={}", deviceId);
}
/**
* 处理电量变化
*/
private void handleBatteryChange(Long deviceId, Object batteryValue) {
log.debug("[DevicePropertyEventHandler] 电量变化: deviceId={}, battery={}", deviceId, batteryValue);
// TODO: 如果电量低于阈值,提示更换
// if (batteryValue instanceof Number && ((Number) batteryValue).intValue() < 20) {
// // 触发低电量提醒
// }
}
/**
* 处理信号强度变化
*/
private void handleSignalChange(Long deviceId, Object signalValue) {
log.debug("[DevicePropertyEventHandler] 信号变化: deviceId={}, signal={}", deviceId, signalValue);
}
/**
* 处理客流数量变化
*/
private void handlePeopleCountChange(Long deviceId, Object peopleCount) {
log.debug("[DevicePropertyEventHandler] 客流变化: deviceId={}, peopleCount={}", deviceId, peopleCount);
// TODO: 客流达标触发保洁
// if (peopleCount instanceof Number && ((Number) peopleCount).intValue() > threshold) {
// // 触发保洁工单
// }
}
}

View File

@@ -1,118 +0,0 @@
package com.viewsh.module.ops.environment.integration.consumer;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viewsh.module.ops.environment.integration.dto.DeviceStatusChangedEventDTO;
import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.springframework.stereotype.Component;
/**
* 设备状态变更事件消费者
* <p>
* 订阅 IoT 模块发布的设备状态变更事件,同步更新保洁员在线状态
* <p>
* RocketMQ 配置:
* - Topic: integration.device.status
* - Tag: productKey可配置如 cleaner_badge_v1
* - 消费模式: CONCURRENTLY并发消费
*
* @author lzh
*/
@Slf4j
@Component
@org.apache.rocketmq.spring.annotation.RocketMQMessageListener(
topic = "integration.device.status",
consumerGroup = "ops-device-status-group",
consumeMode = ConsumeMode.CONCURRENTLY,
// TAG 过滤将在配置文件中设置,或使用 selectorExpression
selectorExpression = "*"
)
public class DeviceStatusEventHandler implements org.apache.rocketmq.spring.core.RocketMQListener<String> {
@Resource
private CleanerStatusService cleanerStatusService;
@Resource
private ObjectMapper objectMapper;
@Resource
private IntegrationEventDeduplicationService deduplicationService;
@Override
public void onMessage(String message) {
try {
// 1. JSON 反序列化
DeviceStatusChangedEventDTO event = objectMapper.readValue(message, DeviceStatusChangedEventDTO.class);
// 2. 幂等性检查
if (!deduplicationService.tryConsume(event.getEventId())) {
log.debug("[DeviceStatusEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId());
return;
}
// 3. 业务处理
handleStatusChanged(event);
} catch (Exception e) {
log.error("[DeviceStatusEventHandler] 消息处理失败: message={}", message, e);
// 抛出异常让 RocketMQ 重试
throw new RuntimeException("设备状态变更事件处理失败", e);
}
}
/**
* 处理设备状态变更
*/
private void handleStatusChanged(DeviceStatusChangedEventDTO event) {
Long deviceId = event.getDeviceId();
Integer newStatus = event.getNewStatus();
log.info("[DeviceStatusEventHandler] 设备状态变更: deviceId={}, newStatus={}, productKey={}",
deviceId, newStatus, event.getProductKey());
// 只处理在线/离线状态变更
// 1 = 在线, 0 = 离线
if (newStatus == 1) {
// 设备上线 -> 保洁员上线
handleDeviceOnline(event);
} else if (newStatus == 0) {
// 设备离线 -> 保洁员离线
handleDeviceOffline(event);
}
log.debug("[DeviceStatusEventHandler] 处理完成: deviceId={}, status={}", deviceId, newStatus);
}
/**
* 处理设备上线
*/
private void handleDeviceOnline(DeviceStatusChangedEventDTO event) {
// TODO: 根据设备ID查找对应的保洁员
// Long cleanerId = getCleanerIdByDeviceId(event.getDeviceId());
// if (cleanerId != null) {
// cleanerStatusService.updateOnlineStatus(cleanerId, true);
// }
log.debug("[DeviceStatusEventHandler] 设备上线: deviceId={}, reason={}",
event.getDeviceId(), event.getReason());
}
/**
* 处理设备离线
*/
private void handleDeviceOffline(DeviceStatusChangedEventDTO event) {
// TODO: 根据设备ID查找对应的保洁员
// Long cleanerId = getCleanerIdByDeviceId(event.getDeviceId());
// if (cleanerId != null) {
// cleanerStatusService.updateOnlineStatus(cleanerId, false);
// }
log.debug("[DeviceStatusEventHandler] 设备离线: deviceId={}, reason={}",
event.getDeviceId(), event.getReason());
}
}

View File

@@ -1,40 +0,0 @@
package com.viewsh.module.ops.environment.integration.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Map;
/**
* 设备事件上报 DTO
* <p>
* 用于反序列化从 RocketMQ 接收的设备事件上报消息
*
* @author lzh
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DeviceEventOccurredEventDTO extends BaseDeviceEventDTO {
/**
* 事件标识符
* 例如: button_click按键点击, sos紧急求救, fall_detected跌倒检测
*/
@JsonProperty("eventIdentifier")
private String eventIdentifier;
/**
* 事件类型
* 例如: ALARM告警事件, CONTROL<EFBC88><E68EA7>事件, INFO信息事件
*/
@JsonProperty("eventType")
private String eventType;
/**
* 事件参数
*/
@JsonProperty("eventParams")
private Map<String, Object> eventParams;
}

View File

@@ -1,33 +0,0 @@
package com.viewsh.module.ops.environment.integration.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Map;
import java.util.Set;
/**
* 设备属性变更事件 DTO
* <p>
* 用于反序列化从 RocketMQ 接收的设备属性变更消息
*
* @author lzh
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DevicePropertyChangedEventDTO extends BaseDeviceEventDTO {
/**
* 变更的属性数据
*/
@JsonProperty("properties")
private Map<String, Object> properties;
/**
* 变更的属性标识符集合
*/
@JsonProperty("changedIdentifiers")
private Set<String> changedIdentifiers;
}

View File

@@ -1,36 +0,0 @@
package com.viewsh.module.ops.environment.integration.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 设备状态变更事件 DTO
* <p>
* 用于反序列化从 RocketMQ 接收的设备状态变更消息
*
* @author lzh
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DeviceStatusChangedEventDTO extends BaseDeviceEventDTO {
/**
* 旧状态
*/
@JsonProperty("oldStatus")
private Integer oldStatus;
/**
* 新状态
*/
@JsonProperty("newStatus")
private Integer newStatus;
/**
* 变更原因
*/
@JsonProperty("reason")
private String reason;
}

View File

@@ -1,160 +0,0 @@
package com.viewsh.module.ops.environment.integration.listener;
import com.viewsh.module.ops.core.event.OrderCompletedEvent;
import com.viewsh.module.ops.core.event.OrderStateChangedEvent;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* 保洁员状态变更监听器
* <p>
* 职责:订阅工单状态变更事件,同步更新保洁员状态
* <p>
* 设计说明:
* - 使用 @EventListener 订阅领域事件
* - 业务逻辑同步执行(保证状态一致性)
* - 通过事件驱动解耦通用层与业务层
* - 只处理保洁类型的工单orderType = "CLEAN"
*
* @author lzh
*/
@Slf4j
@Component
public class CleanerStateChangeListener {
@Resource
private CleanerStatusService cleanerStatusService;
/**
* 订阅状态变更事件
* <p>
* 只处理保洁类型的工单orderType = "CLEAN"
*/
@EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void onOrderStateChanged(OrderStateChangedEvent event) {
// 只处理保洁类型的工单
if (!"CLEAN".equals(event.getOrderType())) {
return;
}
log.info("保洁工单状态变更: orderId={}, {} -> {}, assigneeId={}",
event.getOrderId(), event.getOldStatus(), event.getNewStatus(), event.getOperatorId());
Long assigneeId = event.getOperatorId();
if (assigneeId == null) {
return;
}
switch (event.getNewStatus()) {
case DISPATCHED:
// 派单:不改变保洁员状态(等待保洁员确认)
log.debug("工单已派发,等待保洁员确认: orderId={}, assigneeId={}",
event.getOrderId(), assigneeId);
break;
case CONFIRMED:
// 确认:保洁员变更为 BUSY
updateCleanerStatus(assigneeId, com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY,
"确认工单: " + event.getOrderId());
// 设置当前工单
cleanerStatusService.setCurrentWorkOrder(assigneeId, event.getOrderId(),
event.getOrderCode());
break;
case ARRIVED:
// 到岗:保洁员保持 BUSY
updateCleanerStatus(assigneeId, com.viewsh.module.ops.enums.CleanerStatusEnum.BUSY,
"开始作业: " + event.getOrderId());
break;
case PAUSED:
// 暂停:根据暂停原因决定状态
handlePausedStatus(event, assigneeId);
break;
case COMPLETED:
case CANCELLED:
// 完成/取消:清理当前工单,状态由 autoDispatchNext 决定
cleanerStatusService.clearCurrentWorkOrder(assigneeId);
log.info("工单完成或取消,已清理当前工单: assigneeId={}, orderId={}",
assigneeId, event.getOrderId());
break;
default:
break;
}
}
/**
* 订阅工单完成事件
* <p>
* 在工单完成后自动推送下一个任务或设置保洁员为空闲
*/
@EventListener
@Async("ops-task-executor")
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void onOrderCompleted(OrderCompletedEvent event) {
// 只处理保洁类型的工单
if (!"CLEAN".equals(event.getOrderType())) {
return;
}
log.info("保洁工单完成事件: orderId={}, assigneeId={}, workDuration={}秒",
event.getOrderId(), event.getAssigneeId(), event.getWorkDuration());
Long assigneeId = event.getAssigneeId();
if (assigneeId == null) {
return;
}
// 清理当前工单
cleanerStatusService.clearCurrentWorkOrder(assigneeId);
// 注意状态更新IDLE/BUSY由 autoDispatchNextOrder 处理
// 这里只负责清理当前工单记录
}
// ==================== 私有方法 ====================
/**
* 处理暂停状态
*/
private void handlePausedStatus(OrderStateChangedEvent event, Long assigneeId) {
String interruptReason = event.getPayloadString("interruptReason");
if ("P0_TASK_INTERRUPT".equals(interruptReason)) {
// P0任务打断释放保洁员资源
log.warn("保洁任务被P0任务打断: orderId={}, assigneeId={}",
event.getOrderId(), assigneeId);
cleanerStatusService.clearCurrentWorkOrder(assigneeId);
// 状态保持 BUSY因为有P0任务要处理
} else {
// 普通暂停:保洁员变更为 PAUSED
updateCleanerStatus(assigneeId, com.viewsh.module.ops.enums.CleanerStatusEnum.PAUSED,
event.getRemark() != null ? event.getRemark() : "任务暂停");
}
}
/**
* 更新保洁员状态
*/
private void updateCleanerStatus(Long userId, com.viewsh.module.ops.enums.CleanerStatusEnum newStatus,
String remark) {
try {
cleanerStatusService.updateStatus(userId, newStatus, remark);
log.info("保洁员状态已更新: userId={}, newStatus={}, remark={}",
userId, newStatus, remark);
} catch (Exception e) {
log.error("更新保洁员状态失败: userId={}, newStatus={}", userId, newStatus, e);
// 不抛出异常,避免影响主流程
}
}
}

View File

@@ -1,91 +0,0 @@
package com.viewsh.module.ops.environment.integration.listener;
import com.viewsh.module.ops.core.dispatch.DispatchEngine;
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
import com.viewsh.module.ops.core.event.OrderCreatedEvent;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.PriorityEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* 工单创建事件监听器
* <p>
* 监听工单创建事件,自动触发调度流程
* <p>
* 职责:
* - 工单创建后自动调用调度引擎进行派单
* - 支持 P0 紧急任务的打断逻辑(通过 CleanerPriorityScheduleStrategy
*
* @author AI
*/
@Slf4j
@Component
public class OrderCreatedEventListener {
@Resource
private DispatchEngine dispatchEngine;
@Resource
private OpsOrderMapper opsOrderMapper;
/**
* 监听工单创建事件,触发自动调度
* <p>
* 使用 @TransactionalEventListener 确保在事务提交后才执行调度
* 这样可以避免调度失败导致工单创建回滚
*/
@org.springframework.transaction.event.TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT
)
public void onOrderCreated(OrderCreatedEvent event) {
try {
log.info("[OrderCreatedEventListener] 收到工单创建事件: orderId={}, orderType={}, priority={}",
event.getOrderId(), event.getOrderType(), event.getPriority());
// 只处理保洁工单
if (!"CLEAN".equals(event.getOrderType())) {
log.debug("[OrderCreatedEventListener] 跳过非保洁工单: orderType={}", event.getOrderType());
return;
}
// 查询工单信息
OpsOrderDO order = opsOrderMapper.selectById(event.getOrderId());
if (order == null) {
log.warn("[OrderCreatedEventListener] 工单不存在,无法调度: orderId={}", event.getOrderId());
return;
}
// 构建调度上下文
OrderDispatchContext context = OrderDispatchContext.builder()
.orderId(order.getId())
.orderCode(order.getOrderCode())
.orderTitle(order.getTitle())
.businessType(order.getOrderType())
.areaId(event.getAreaId() != null ? event.getAreaId() : order.getAreaId())
.priority(PriorityEnum.fromPriority(event.getPriority()))
.build();
// 使用调度引擎执行调度(包含 P0 打断逻辑)
var result = dispatchEngine.dispatch(context);
if (result.isSuccess()) {
log.info("[OrderCreatedEventListener] 自动调度成功: orderId={}, assigneeId={}, path={}",
event.getOrderId(), result.getAssigneeId(), result.getPath());
} else {
log.warn("[OrderCreatedEventListener] 自动调度失败: orderId={}, reason={}",
event.getOrderId(), result.getMessage());
}
} catch (Exception e) {
// 调度失败不应影响工单创建
log.error("[OrderCreatedEventListener] 自动调度异常: orderId={}", event.getOrderId(), e);
}
}
}

View File

@@ -0,0 +1,107 @@
package com.viewsh.module.ops.environment.service.cleanorder;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
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.service.OrderDetailVO;
import com.viewsh.module.ops.service.OrderExtQueryHandler;
import com.viewsh.module.ops.service.OrderSummaryVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 保洁工单扩展查询处理器
* <p>
* 实现 OrderExtQueryHandler 接口,为工单中心查询提供保洁扩展信息加载能力
*
* @author lzh
*/
@Slf4j
@Component
public class CleanOrderExtQueryHandler implements OrderExtQueryHandler {
@Resource
private OpsOrderCleanExtMapper cleanExtMapper;
/**
* 支持的工单类型
*/
private static final String ORDER_TYPE_CLEAN = "CLEAN";
@Override
public boolean supports(String orderType) {
return ORDER_TYPE_CLEAN.equals(orderType);
}
@Override
public void enrichWithExtInfo(OrderSummaryVO vo, Long orderId) {
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);
if (cleanExt != null) {
Map<String, Object> extInfo = new HashMap<>();
extInfo.put("isAuto", cleanExt.getIsAuto() != null && cleanExt.getIsAuto() == 1);
extInfo.put("expectedDuration", cleanExt.getExpectedDuration());
extInfo.put("cleaningType", cleanExt.getCleaningType());
extInfo.put("difficultyLevel", cleanExt.getDifficultyLevel());
extInfo.put("arrivedTime", cleanExt.getArrivedTime());
extInfo.put("completedTime", cleanExt.getCompletedTime());
extInfo.put("totalPauseSeconds", cleanExt.getTotalPauseSeconds());
vo.setExtInfo(extInfo);
}
}
@Override
public OrderDetailVO buildDetailVO(OpsOrderDO order) {
// 查询扩展信息
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(order.getId());
// 构建基础VO
OrderDetailVO vo = OrderDetailVO.builder()
.id(order.getId())
.orderCode(order.getOrderCode())
.orderType(order.getOrderType())
.sourceType(order.getSourceType())
.title(order.getTitle())
.description(order.getDescription())
.priority(order.getPriority())
.status(order.getStatus())
.areaId(order.getAreaId())
.location(order.getLocation())
.urgentReason(order.getUrgentReason())
.assigneeId(order.getAssigneeId())
.inspectorId(order.getInspectorId())
.startTime(order.getStartTime())
.endTime(order.getEndTime())
.qualityScore(order.getQualityScore())
.qualityComment(order.getQualityComment())
.responseSeconds(order.getResponseSeconds())
.completionSeconds(order.getCompletionSeconds())
.createTime(order.getCreateTime())
.updateTime(order.getUpdateTime())
.triggerSource(order.getTriggerSource())
.triggerRuleId(order.getTriggerRuleId())
.triggerDeviceId(order.getTriggerDeviceId())
.triggerDeviceKey(order.getTriggerDeviceKey())
.build();
// 填充扩展信息
if (cleanExt != null) {
Map<String, Object> extInfo = new HashMap<>();
extInfo.put("isAuto", cleanExt.getIsAuto() != null && cleanExt.getIsAuto() == 1);
extInfo.put("expectedDuration", cleanExt.getExpectedDuration());
extInfo.put("cleaningType", cleanExt.getCleaningType());
extInfo.put("difficultyLevel", cleanExt.getDifficultyLevel());
extInfo.put("arrivedTime", cleanExt.getArrivedTime());
extInfo.put("completedTime", cleanExt.getCompletedTime());
extInfo.put("pauseStartTime", cleanExt.getPauseStartTime());
extInfo.put("pauseEndTime", cleanExt.getPauseEndTime());
extInfo.put("totalPauseSeconds", cleanExt.getTotalPauseSeconds());
vo.setExtInfo(extInfo);
}
return vo;
}
}

View File

@@ -14,7 +14,7 @@ import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqD
* <p>
* 变更说明:
* - 移除了暂停/恢复/打断方法(由 {@link com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager} 处理)
* - 移除了作业时长计算方法(由 {@link com.viewsh.module.ops.environment.handler.CleanOrderEventHandler} 通过事件处理)
* - 移除了作业时长计算方法(由 {@link com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener} 通过事件处理)
*
* @author lzh
*/

View File

@@ -17,10 +17,9 @@ import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO;
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.handler.CleanOrderEventHandler;
import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService;
import com.viewsh.module.ops.environment.service.dispatch.CleanerAreaAssignStrategy;
import com.viewsh.module.ops.service.order.OpsOrderService;
import com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener;
import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -30,23 +29,19 @@ import java.time.Duration;
import java.time.LocalDateTime;
/**
* 保洁工单服务实现(简化版)
* 保洁工单服务实现(重构版)
* <p>
* 职责:
* 1. 工单创建与自动分配
* 1. 工单创建与自动分配(直接操作主表+扩展表)
* 2. 保洁特有的状态转换编排confirm, arrive, complete
* 3. 作业时长计算
* <p>
* 架构说明:
* - 状态同步:委托给 {@link OrderLifecycleManager}
* - 派单决策:使用新的 {@link DispatchEngine}
* - 保洁员状态:由 {@link com.viewsh.module.ops.environment.integration.listener.CleanerStateChangeListener} 处理
* - 通知逻辑:由 {@link CleanOrderEventHandler} 处理
* <p>
* 变更历史:
* - Phase 3: 使用新的调度引擎和生命周期管理器
* - Phase 3: 移除直接的状态管理代码(改为事件驱动)
* - Phase 3: 瘦身代码,专注保洁业务编排
* - 直接操作主表和扩展表,不依赖 OpsOrderService
* - 使用公用组件 OrderIdGenerator、OrderCodeGenerator
* - 使用事件发布器 OrderEventPublisher
* - 状态同步委托给 OrderLifecycleManager
* - 派单决策使用 DispatchEngine
*
* @author lzh
*/
@@ -54,9 +49,6 @@ import java.time.LocalDateTime;
@Service
public class CleanOrderServiceImpl implements CleanOrderService {
@Resource
private OpsOrderService opsOrderService;
@Resource
private OpsOrderMapper opsOrderMapper;
@@ -64,13 +56,16 @@ public class CleanOrderServiceImpl implements CleanOrderService {
private OpsOrderCleanExtMapper cleanExtMapper;
@Resource
private OrderQueueService orderQueueService;
private OrderIdGenerator orderIdGenerator;
// @Resource
// private CleanerAreaAssignStrategy cleanerAreaAssignStrategy;
//
// @Resource
// private CleanerStatusService cleanerStatusService;
@Resource
private OrderCodeGenerator orderCodeGenerator;
@Resource
private OrderEventPublisher orderEventPublisher;
@Resource
private OrderQueueService orderQueueService;
@Resource
private DispatchEngine dispatchEngine;
@@ -79,20 +74,40 @@ public class CleanOrderServiceImpl implements CleanOrderService {
private OrderLifecycleManager orderLifecycleManager;
@Resource
private CleanOrderEventHandler cleanOrderEventHandler;
@Resource
private OrderEventPublisher orderEventPublisher;
private CleanOrderEventListener cleanOrderEventListener;
// ==================== 工单创建 ====================
@Override
@Transactional(rollbackFor = Exception.class)
public Long createAutoCleanOrder(CleanOrderAutoCreateReqDTO createReq) {
// 1. 调用基础服务创建工单
Long orderId = opsOrderService.createOrder(createReq);
// 1. 生成ID和编号
Long orderId = orderIdGenerator.generate();
String orderCode = orderCodeGenerator.generate("CLEAN");
// 2. 创建保洁扩展信息
// 2. 构建主表数据
OpsOrderDO order = OpsOrderDO.builder()
.id(orderId)
.orderCode(orderCode)
.orderType("CLEAN")
.title(createReq.getTitle())
.description(createReq.getDescription())
.priority(createReq.getPriority() != null ? createReq.getPriority() : PriorityEnum.P2.getPriority())
.status(WorkOrderStatusEnum.PENDING.getStatus())
.areaId(createReq.getAreaId())
.location(createReq.getLocation())
.sourceType(createReq.getSourceType() != null ? createReq.getSourceType() : "TRAFFIC")
// IoT集成字段
.triggerSource(createReq.getTriggerSource())
.triggerRuleId(createReq.getTriggerRuleId())
.triggerDeviceId(createReq.getTriggerDeviceId())
.triggerDeviceKey(createReq.getTriggerDeviceKey())
.build();
// 3. 插入主表
opsOrderMapper.insert(order);
// 4. 构建扩展表数据(同一事务)
OpsOrderCleanExtDO cleanExt = OpsOrderCleanExtDO.builder()
.opsOrderId(orderId)
.isAuto(1)
@@ -102,62 +117,29 @@ public class CleanOrderServiceImpl implements CleanOrderService {
.build();
cleanExtMapper.insert(cleanExt);
log.info("创建自动保洁工单成功: orderId={}, expectedDuration={}分钟",
orderId, createReq.getExpectedDuration());
log.info("创建自动保洁工单成功: orderId={}, orderCode={}, expectedDuration={}分钟, triggerSource={}",
orderId, orderCode, createReq.getExpectedDuration(), createReq.getTriggerSource());
// 3. 发布工单创建事件,由 OrderCreatedEventListener 触发调度
// 5. 发布工单创建事件,由 CleanOrderEventListener 触发调度
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(orderId)
.orderType("CLEAN")
.orderCode(orderCode)
.title(createReq.getTitle())
.areaId(createReq.getAreaId())
.priority(PriorityEnum.fromPriority(createReq.getPriority()).getPriority())
.createTime(java.time.LocalDateTime.now())
.priority(order.getPriority())
.createTime(LocalDateTime.now())
.build()
.addPayload("isAuto", true)
.addPayload("expectedDuration", createReq.getExpectedDuration());
.addPayload("expectedDuration", createReq.getExpectedDuration())
.addPayload("triggerSource", createReq.getTriggerSource());
orderEventPublisher.publishOrderCreated(event);
return orderId;
}
/**
* 自动分配工单(使用新的调度引擎)
*/
private void autoAssignOrder(Long orderId, Long areaId, PriorityEnum priority) {
try {
// 查询工单信息
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
log.warn("工单不存在,无法自动分配: orderId={}", orderId);
return;
}
// 构建调度上下文
OrderDispatchContext context = OrderDispatchContext.builder()
.orderId(order.getId())
.orderCode(order.getOrderCode())
.orderTitle(order.getTitle())
.businessType(order.getOrderType())
.areaId(areaId)
.priority(priority)
.build();
// 使用调度引擎执行调度
DispatchResult result = dispatchEngine.dispatch(context);
if (result.isSuccess()) {
log.info("自动分配成功: orderId={}, assigneeId={}, path={}",
orderId, result.getAssigneeId(), result.getPath());
} else {
log.warn("自动分配失败: orderId={}, reason={}", orderId, result.getMessage());
}
} catch (Exception e) {
log.error("自动分配工单失败: orderId={}", orderId, e);
}
}
// ==================== 队列推送(兼容旧接口)====================
// ==================== 队列推送 ====================
@Override
@Transactional(rollbackFor = Exception.class)
@@ -210,7 +192,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
}
// 语音播报
cleanOrderEventHandler.sendNewOrderNotification(cleanerId, orderId);
cleanOrderEventListener.sendNewOrderNotification(cleanerId, orderId);
}
@Override
@@ -272,7 +254,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
DispatchResult result = dispatchEngine.urgentInterrupt(orderId, queueDTO.getUserId());
// 6. 发送优先级升级通知
cleanOrderEventHandler.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode());
cleanOrderEventListener.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode());
return result.isSuccess();
}
@@ -298,7 +280,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
.build();
orderLifecycleManager.transition(request);
// 注意:保洁员状态更新由 CleanerStateChangeListener 处理
// 注意:保洁员状态更新由 CleanOrderEventListener 处理
}
@Override
@@ -362,7 +344,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
log.info("已自动推送下一个任务: cleanerId={}", cleanerId);
} else {
log.info("无等待任务,保洁员变空闲: cleanerId={}", cleanerId);
// 状态更新由 CleanerStateChangeListener 处理
// 状态更新由 CleanOrderEventListener 处理
}
}
@@ -370,17 +352,17 @@ public class CleanOrderServiceImpl implements CleanOrderService {
@Override
public void playVoiceForNewOrder(Long cleanerId) {
cleanOrderEventHandler.sendNewOrderNotification(cleanerId, null);
cleanOrderEventListener.sendNewOrderNotification(cleanerId, null);
}
@Override
public void playVoiceForQueuedOrder(Long cleanerId, int queueCount) {
cleanOrderEventHandler.sendQueuedOrderNotification(cleanerId, queueCount);
cleanOrderEventListener.sendQueuedOrderNotification(cleanerId, queueCount);
}
@Override
public void playVoiceForNextTask(Long cleanerId, int queueCount, String nextTaskTitle) {
cleanOrderEventHandler.sendNextTaskNotification(cleanerId, queueCount, nextTaskTitle);
cleanOrderEventListener.sendNextTaskNotification(cleanerId, queueCount, nextTaskTitle);
}
// ==================== 作业时长计算 ====================
@@ -403,7 +385,6 @@ public class CleanOrderServiceImpl implements CleanOrderService {
/**
* 记录到岗时间
* 注意:此方法在被 @Transactional 的公共方法调用时,会参与同一事务
*/
private void recordArrivedTime(Long orderId) {
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);
@@ -418,7 +399,6 @@ public class CleanOrderServiceImpl implements CleanOrderService {
/**
* 记录完成时间
* 注意:此方法在被 @Transactional 的公共方法调用时,会参与同一事务
*/
private void recordCompletedTime(Long orderId) {
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);

View File

@@ -2,6 +2,7 @@ package com.viewsh.module.ops.environment.service.dispatch;
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.core.dispatch.model.AssigneeStatus;
import com.viewsh.module.ops.core.dispatch.model.DispatchDecision;
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
@@ -46,7 +47,7 @@ public class CleanerPriorityScheduleStrategy implements ScheduleStrategy {
private OrderQueueService orderQueueService;
@Resource
private com.viewsh.module.ops.core.dispatch.DispatchEngine dispatchEngine;
private DispatchEngine dispatchEngine;
@PostConstruct
public void init() {

View File

@@ -0,0 +1,203 @@
package com.viewsh.module.ops.environment.service.cleanorder;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderCleanExtDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderCleanExtMapper;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.service.OrderDetailVO;
import com.viewsh.module.ops.service.OrderSummaryVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* 保洁工单扩展查询处理器测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
class CleanOrderExtQueryHandlerTest {
@Mock
private OpsOrderCleanExtMapper cleanExtMapper;
@InjectMocks
private CleanOrderExtQueryHandler handler;
private OpsOrderDO testOrder;
private OpsOrderCleanExtDO testCleanExt;
@BeforeEach
void setUp() {
// 初始化测试工单
testOrder = OpsOrderDO.builder()
.id(1L)
.orderCode("CLEAN-20250119-0001")
.orderType("CLEAN")
.status(WorkOrderStatusEnum.PENDING.getStatus())
.title("2楼电梯厅保洁")
.priority(PriorityEnum.P2.getPriority())
.areaId(100L)
.location("2楼电梯厅")
.createTime(LocalDateTime.now())
.build();
// 初始化测试扩展信息
testCleanExt = OpsOrderCleanExtDO.builder()
.id(1L)
.opsOrderId(1L)
.isAuto(1)
.expectedDuration(30)
.cleaningType("DEEP")
.difficultyLevel(3)
.arrivedTime(LocalDateTime.now().minusHours(1))
.completedTime(LocalDateTime.now())
.totalPauseSeconds(120)
.build();
}
@Test
void testSupports_ReturnsTrueForClean() {
// When & Then
assertTrue(handler.supports("CLEAN"));
}
@Test
void testSupports_ReturnsFalseForOtherTypes() {
// When & Then
assertFalse(handler.supports("SECURITY"));
assertFalse(handler.supports("FACILITIES"));
assertFalse(handler.supports("SERVICE"));
assertFalse(handler.supports(null));
}
@Test
void testEnrichWithExtInfo_ExistingExt_FillsExtInfo() {
// Given
when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(testCleanExt);
OrderSummaryVO vo = OrderSummaryVO.builder()
.id(1L)
.orderCode("CLEAN-20250119-0001")
.orderType("CLEAN")
.build();
// When
handler.enrichWithExtInfo(vo, 1L);
// Then
assertNotNull(vo.getExtInfo());
assertEquals(true, vo.getExtInfo().get("isAuto"));
assertEquals(30, vo.getExtInfo().get("expectedDuration"));
assertEquals("DEEP", vo.getExtInfo().get("cleaningType"));
assertEquals(3, vo.getExtInfo().get("difficultyLevel"));
assertNotNull(vo.getExtInfo().get("arrivedTime"));
assertNotNull(vo.getExtInfo().get("completedTime"));
assertEquals(120, vo.getExtInfo().get("totalPauseSeconds"));
}
@Test
void testEnrichWithExtInfo_NoExistingExt_EmptyExtInfo() {
// Given
when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(null);
OrderSummaryVO vo = OrderSummaryVO.builder()
.id(1L)
.orderCode("CLEAN-20250119-0001")
.orderType("CLEAN")
.build();
// When
handler.enrichWithExtInfo(vo, 1L);
// Then
assertNotNull(vo.getExtInfo());
assertTrue(vo.getExtInfo().isEmpty());
}
@Test
void testEnrichWithExtInfo_IsAutoZero_FillsAsFalse() {
// Given
testCleanExt.setIsAuto(0);
when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(testCleanExt);
OrderSummaryVO vo = OrderSummaryVO.builder()
.id(1L)
.orderCode("CLEAN-20250119-0001")
.orderType("CLEAN")
.build();
// When
handler.enrichWithExtInfo(vo, 1L);
// Then
assertEquals(false, vo.getExtInfo().get("isAuto"));
}
@Test
void testBuildDetailVO_ExistingExt_ReturnsDetailedVO() {
// Given
when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(testCleanExt);
// When
OrderDetailVO vo = handler.buildDetailVO(testOrder);
// Then: 验证基础字段
assertEquals(1L, vo.getId());
assertEquals("CLEAN-20250119-0001", vo.getOrderCode());
assertEquals("CLEAN", vo.getOrderType());
assertEquals("2楼电梯厅保洁", vo.getTitle());
assertEquals(PriorityEnum.P2.getPriority(), vo.getPriority());
assertEquals(WorkOrderStatusEnum.PENDING.getStatus(), vo.getStatus());
assertEquals(100L, vo.getAreaId());
assertEquals("2楼电梯厅", vo.getLocation());
// 验证扩展字段
assertNotNull(vo.getExtInfo());
assertEquals(true, vo.getExtInfo().get("isAuto"));
assertEquals(30, vo.getExtInfo().get("expectedDuration"));
assertEquals("DEEP", vo.getExtInfo().get("cleaningType"));
assertEquals(3, vo.getExtInfo().get("difficultyLevel"));
}
@Test
void testBuildDetailVO_NoExistingExt_ReturnsVOWithEmptyExtInfo() {
// Given
when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(null);
// When
OrderDetailVO vo = handler.buildDetailVO(testOrder);
// Then: 基础字段仍然存在
assertEquals(1L, vo.getId());
assertEquals("CLEAN-20250119-0001", vo.getOrderCode());
// 扩展信息为空Map
assertNotNull(vo.getExtInfo());
assertTrue(vo.getExtInfo().isEmpty());
}
@Test
void testBuildDetailVO_IncludesAllExtFields() {
// Given
testCleanExt.setPauseStartTime(LocalDateTime.now().minusMinutes(30));
testCleanExt.setPauseEndTime(LocalDateTime.now().minusMinutes(28));
when(cleanExtMapper.selectByOpsOrderId(1L)).thenReturn(testCleanExt);
// When
OrderDetailVO vo = handler.buildDetailVO(testOrder);
// Then: 验证所有扩展字段都被包含
assertNotNull(vo.getExtInfo().get("arrivedTime"));
assertNotNull(vo.getExtInfo().get("completedTime"));
assertNotNull(vo.getExtInfo().get("pauseStartTime"));
assertNotNull(vo.getExtInfo().get("pauseEndTime"));
assertNotNull(vo.getExtInfo().get("totalPauseSeconds"));
}
}