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

View File

@@ -58,10 +58,10 @@ public interface OrderLifecycleManager {
OrderTransitionResult enqueue(OrderTransitionRequest request);
/**
* 工单出队并派单QUEUED → DISPATCHED
* 工单出队并派单:PENDING/QUEUED → DISPATCHED
* <p>
* 状态转换:
* - 工单状态QUEUED → DISPATCHED
* - 工单状态:PENDING/QUEUED → DISPATCHED
* - 队列状态WAITING → PROCESSING
*
* @param request 状态转换请求

View File

@@ -0,0 +1,93 @@
package com.viewsh.module.ops.infrastructure.code;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
/**
* 工单编号生成器
* <p>
* 格式:{业务前缀}-{日期}-{序号}
* 例如CLEAN-20250119-0001, SECURITY-20250119-0001
* <p>
* 特性:
* - 使用 Redis 保证序号唯一性
* - 序号每日自动重置(按日期分 key
* - 不同业务类型独立计数
*
* @author lzh
*/
@Slf4j
@Service
public class OrderCodeGenerator {
private static final String KEY_PREFIX = "ops:order:code:";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final long DEFAULT_EXPIRE_DAYS = 7; // key 默认保留7天
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 生成工单编号
*
* @param businessType 业务类型CLEAN、SECURITY、FACILITIES
* @return 工单编号,格式:{业务类型}-{日期}-{4位序号}
*/
public String generate(String businessType) {
if (businessType == null || businessType.isEmpty()) {
throw new IllegalArgumentException("Business type cannot be null or empty");
}
String dateStr = LocalDate.now().format(DATE_FORMATTER);
String key = KEY_PREFIX + businessType + ":" + dateStr;
// Redis 自增并获取新值
Long seq = stringRedisTemplate.opsForValue().increment(key);
// 首次创建时设置过期时间
if (seq != null && seq == 1) {
stringRedisTemplate.expire(key, DEFAULT_EXPIRE_DAYS, TimeUnit.DAYS);
}
if (seq == null) {
throw new RuntimeException("Failed to generate order code for business type: " + businessType);
}
String orderCode = String.format("%s-%s-%04d", businessType, dateStr, seq);
log.debug("生成工单编号: businessType={}, orderCode={}", businessType, orderCode);
return orderCode;
}
/**
* 获取指定业务类型当天的当前序号
*
* @param businessType 业务类型
* @return 当前序号如果不存在返回0
*/
public long getCurrentSeq(String businessType) {
String dateStr = LocalDate.now().format(DATE_FORMATTER);
String key = KEY_PREFIX + businessType + ":" + dateStr;
String value = stringRedisTemplate.opsForValue().get(key);
return value == null ? 0 : Long.parseLong(value);
}
/**
* 重置指定业务类型当天的序号(仅供测试或特殊场景使用)
*
* @param businessType 业务类型
*/
public void reset(String businessType) {
String dateStr = LocalDate.now().format(DATE_FORMATTER);
String key = KEY_PREFIX + businessType + ":" + dateStr;
stringRedisTemplate.delete(key);
log.warn("重置工单编号序号: businessType={}, date={}", businessType, dateStr);
}
}

View File

@@ -0,0 +1,55 @@
package com.viewsh.module.ops.infrastructure.id;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 工单ID生成器
* <p>
* 使用雪花算法,保证分布式环境下的全局唯一性。
* 适用于工单主键生成。
*
* @author lzh
*/
@Slf4j
@Component
public class OrderIdGenerator {
private final OrderIdProperties orderIdProperties;
private SnowflakeIdGenerator snowflake;
/**
* 构造函数,注入配置
*/
public OrderIdGenerator(OrderIdProperties orderIdProperties) {
this.orderIdProperties = orderIdProperties;
}
/**
* 初始化雪花算法生成器
*/
@PostConstruct
public void init() {
int datacenterId = orderIdProperties.getDatacenterId();
int machineId = orderIdProperties.getMachineId();
this.snowflake = new SnowflakeIdGenerator(datacenterId, machineId);
log.info("OrderIdGenerator初始化完成: datacenterId={}, machineId={}",
datacenterId, machineId);
}
/**
* 生成工单ID
*
* @return 唯一的Long类型ID
*/
public Long generate() {
if (snowflake == null) {
throw new IllegalStateException("OrderIdGenerator 尚未初始化");
}
return snowflake.nextId();
}
}

View File

@@ -0,0 +1,28 @@
package com.viewsh.module.ops.infrastructure.id;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 工单ID生成器<E68890><E599A8>
*
* @author lzh
*/
@Data
@Component
@ConfigurationProperties(prefix = "ops.order")
public class OrderIdProperties {
/**
* 数据中心ID0-31
* 默认值1
*/
private int datacenterId = 1;
/**
* 机器ID0-31
* 默认值1
*/
private int machineId = 1;
}

View File

@@ -0,0 +1,184 @@
package com.viewsh.module.ops.infrastructure.id;
/**
* 雪花算法ID生成器
* <p>
* ID<49><44><EFBFBD>64位Long
* - 1位符号位永远为0
* - 41位时间戳毫秒级可用69年
* - 5位数据中心ID0-31
* - 5位机器ID0-31
* - 12位序列号毫秒内计数0-4095
* <p>
* 特性:
* - 分布式环境全局唯一
* - 时间有序
* - 高性能单机每毫秒可生成4096个ID
*
* @author lzh
*/
public class SnowflakeIdGenerator {
/**
* 起始时间戳2024-01-01 00:00:00
* 可根据项目实际情况调整
*/
private static final long EPOCH = 1704067200000L;
/**
* 各部分位数
*/
private static final long DATACENTER_ID_BITS = 5L;
private static final long MACHINE_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
/**
* 各部分最大值
*/
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); // 31
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS); // 31
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095
/**
* 各部分位移
*/
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATACENTER_ID_BITS;
/**
* 数据中心ID
*/
private final long datacenterId;
/**
* 机器ID
*/
private final long machineId;
/**
* 序列号
*/
private long sequence = 0L;
/**
* 上次生成ID的时间戳
*/
private long lastTimestamp = -1L;
/**
* 构造函数
*
* @param datacenterId 数据中心ID0-31
* @param machineId 机器ID0-31
* @throws IllegalArgumentException 如果ID超出范围
*/
public SnowflakeIdGenerator(long datacenterId, long machineId) {
if (datacenterId < 0 || datacenterId > MAX_DATACENTER_ID) {
throw new IllegalArgumentException(
String.format("Datacenter ID must be between 0 and %d", MAX_DATACENTER_ID));
}
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
throw new IllegalArgumentException(
String.format("Machine ID must be between 0 and %d", MAX_MACHINE_ID));
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
/**
* 生成下一个ID线程安全
*
* @return 唯一的Long类型ID
*/
public synchronized long nextId() {
long timestamp = getCurrentTimestamp();
// 时钟回拨检查
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 少量时钟回拨,等待
try {
Thread.sleep(offset << 1);
timestamp = getCurrentTimestamp();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate ID");
}
} catch (InterruptedException e) {
throw new RuntimeException("Clock moved backwards. Waiting interrupted", e);
}
} else {
throw new RuntimeException("Clock moved backwards. Refusing to generate ID");
}
}
// 同一毫秒内,序列号自增
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 新的毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装ID
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
/**
* 获取当前时间戳
*/
private long getCurrentTimestamp() {
return System.currentTimeMillis();
}
/**
* 等待下一毫秒
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = getCurrentTimestamp();
while (timestamp <= lastTimestamp) {
timestamp = getCurrentTimestamp();
}
return timestamp;
}
/**
* 解析ID获取时间戳信息
*
* @param id 雪花算法生成的ID
* @return 原始时间戳(毫秒)
*/
public static long parseTimestamp(long id) {
return ((id >> TIMESTAMP_SHIFT) & ~(-1L << 41L)) + EPOCH;
}
/**
* 解析ID获取数据中心ID
*
* @param id 雪花算法生成的ID
* @return 数据中心ID
*/
public static long parseDatacenterId(long id) {
return (id >> DATACENTER_ID_SHIFT) & MAX_DATACENTER_ID;
}
/**
* 解析ID获取机器ID
*
* @param id 雪花算法生成的ID
* @return 机器ID
*/
public static long parseMachineId(long id) {
return (id >> MACHINE_ID_SHIFT) & MAX_MACHINE_ID;
}
}

View File

@@ -0,0 +1,164 @@
package com.viewsh.module.ops.service;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 工单详情 VO基类
* <p>
* 支持多态,具体业务类型可继承此类添加扩展字段
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderDetailVO {
/**
* 工单ID
*/
private Long id;
/**
* 工单编号
*/
private String orderCode;
/**
* 工单类型
*/
private String orderType;
/**
* 来源类型
*/
private String sourceType;
/**
* 工单标题
*/
private String title;
/**
* 工单描述
*/
private String description;
/**
* 优先级
*/
private Integer priority;
/**
* 工单状态
*/
private String status;
/**
* 区域ID
*/
private Long areaId;
/**
* 具体位置描述
*/
private String location;
/**
* 加急原因
*/
private String urgentReason;
/**
* 当前执行人ID
*/
private Long assigneeId;
/**
* 当前执行人姓名
*/
private String assigneeName;
/**
* 巡检员ID
*/
private Long inspectorId;
/**
* 巡检员姓名
*/
private String inspectorName;
/**
* 工单开始时间
*/
private LocalDateTime startTime;
/**
* 工单结束时间
*/
private LocalDateTime endTime;
/**
* 验收评分
*/
private Integer qualityScore;
/**
* 验收评语
*/
private String qualityComment;
/**
* 响应耗时(秒)
*/
private Integer responseSeconds;
/**
* 完成耗时(秒)
*/
private Integer completionSeconds;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 触发来源
*/
private String triggerSource;
/**
* 触发规则ID
*/
private Long triggerRuleId;
/**
* 触发设备ID
*/
private Long triggerDeviceId;
/**
* 触发设备Key
*/
private String triggerDeviceKey;
/**
* 扩展信息Map形式通用字段
*/
@Builder.Default
private Map<String, Object> extInfo = new java.util.HashMap<>();
}

View File

@@ -0,0 +1,46 @@
package com.viewsh.module.ops.service;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import java.util.Map;
/**
* 工单扩展表查询处理器接口
* <p>
* 各业务模块实现此接口,提供对应业务类型的扩展信息加载能力。
* 例如:
* - environment-biz 实现 CleanOrderExtQueryHandler
* - security-biz 实现 SecurityOrderExtQueryHandler
*
* @author lzh
*/
public interface OrderExtQueryHandler {
/**
* 是否支持此业务类型
*
* @param orderType 工单类型CLEAN、SECURITY、FACILITIES
* @return true-支持false-不支持
*/
boolean supports(String orderType);
/**
* 为汇总VO填充扩展信息
* <p>
* 用于分页查询场景,将扩展信息填充到 extInfo Map 中
*
* @param vo 汇总VO
* @param orderId 工单ID
*/
void enrichWithExtInfo(OrderSummaryVO vo, Long orderId);
/**
* 构建详情VO
* <p>
* 用于详情查询场景返回包含完整扩展信息的详情VO
*
* @param order 工单主表数据
* @return 详情VO
*/
OrderDetailVO buildDetailVO(OpsOrderDO order);
}

View File

@@ -0,0 +1,220 @@
package com.viewsh.module.ops.service;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import java.util.Map;
/**
* 工单中心查询服务接口
* <p>
* 职责:
* 1. 综合分页查询(支持所有业务类型)
* 2. 工单详情查询(含完整扩展信息)
* 3. 工单统计信息
*
* @author lzh
*/
public interface OrderQueryService {
/**
* 分页查询工单(支持所有业务类型)
* <p>
* 查询主表数据后,自动调用对应的扩展查询处理器填充扩展信息
*
* @param query 查询条件
* @return 分页结果
*/
PageResult<OrderSummaryVO> queryPage(OrderQuery query);
/**
* 查询工单详情(含完整扩展信息)
* <p>
* 根据工单的业务类型,调用对应的扩展查询处理器加载完整扩展信息
*
* @param orderId 工单ID
* @return 详情VO
*/
OrderDetailVO getDetail(Long orderId);
/**
* 获取工单统计信息
* <p>
* 按业务类型分组返回工单数量统计
*
* @param groupBy 分组维度status按状态统计null只按类型统计
* @return 统计结果
*/
Map<String, Object> getStats(String groupBy);
/**
* 工单查询条件
*/
class OrderQuery {
/**
* 页码从1开始
*/
private Integer page = 1;
/**
* 每页大小
*/
private Integer size = 20;
/**
* 工单类型(可选)
*/
private String orderType;
/**
* 工单状态(可选)
*/
private String status;
/**
* 优先级(可选)
*/
private Integer priority;
/**
* 区域ID可选
*/
private Long areaId;
/**
* 执行人ID可选
*/
private Long assigneeId;
/**
* 工单编号模糊查询(可选)
*/
private String orderCode;
/**
* 标题模糊查询(可选)
*/
private String title;
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
public String getOrderType() {
return orderType;
}
public void setOrderType(String orderType) {
this.orderType = orderType;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Integer getPriority() {
return priority;
}
public void setPriority(Integer priority) {
this.priority = priority;
}
public Long getAreaId() {
return areaId;
}
public void setAreaId(Long areaId) {
this.areaId = areaId;
}
public Long getAssigneeId() {
return assigneeId;
}
public void setAssigneeId(Long assigneeId) {
this.assigneeId = assigneeId;
}
public String getOrderCode() {
return orderCode;
}
public void setOrderCode(String orderCode) {
this.orderCode = orderCode;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public static OrderQuery builder() {
return new OrderQuery();
}
public OrderQuery withPage(Integer page) {
this.page = page;
return this;
}
public OrderQuery withSize(Integer size) {
this.size = size;
return this;
}
public OrderQuery withOrderType(String orderType) {
this.orderType = orderType;
return this;
}
public OrderQuery withStatus(String status) {
this.status = status;
return this;
}
public OrderQuery withPriority(Integer priority) {
this.priority = priority;
return this;
}
public OrderQuery withAreaId(Long areaId) {
this.areaId = areaId;
return this;
}
public OrderQuery withAssigneeId(Long assigneeId) {
this.assigneeId = assigneeId;
return this;
}
public OrderQuery withOrderCode(String orderCode) {
this.orderCode = orderCode;
return this;
}
public OrderQuery withTitle(String title) {
this.title = title;
return this;
}
}
}

View File

@@ -0,0 +1,186 @@
package com.viewsh.module.ops.service;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 工单中心查询服务实现
*
* @author lzh
*/
@Slf4j
@Service
public class OrderQueryServiceImpl implements OrderQueryService {
@Resource
private OpsOrderMapper opsOrderMapper;
/**
* 扩展查询处理器列表(由各业务模块注入)
*/
private final List<OrderExtQueryHandler> extQueryHandlers;
public OrderQueryServiceImpl(List<OrderExtQueryHandler> extQueryHandlers) {
this.extQueryHandlers = extQueryHandlers;
log.info("OrderQueryService 初始化,注册 {} 个扩展查询处理器", extQueryHandlers.size());
}
@Override
public PageResult<OrderSummaryVO> queryPage(OrderQuery query) {
// 1. 构建查询条件
LambdaQueryWrapperX<OpsOrderDO> wrapper = new LambdaQueryWrapperX<OpsOrderDO>()
.eqIfPresent(OpsOrderDO::getOrderType, query.getOrderType())
.eqIfPresent(OpsOrderDO::getStatus, query.getStatus())
.eqIfPresent(OpsOrderDO::getPriority, query.getPriority())
.eqIfPresent(OpsOrderDO::getAreaId, query.getAreaId())
.eqIfPresent(OpsOrderDO::getAssigneeId, query.getAssigneeId())
.likeIfPresent(OpsOrderDO::getOrderCode, query.getOrderCode())
.likeIfPresent(OpsOrderDO::getTitle, query.getTitle())
.orderByDesc(OpsOrderDO::getCreateTime);
// 2. 分页查询主表
PageResult<OpsOrderDO> pageResult = opsOrderMapper.selectPage(query.getPage(), query.getSize(), wrapper);
// 3. <20><><EFBFBD>换为 VO 并填充扩展信息
List<OrderSummaryVO> voList = pageResult.getList().stream()
.map(this::convertToSummaryVO)
.toList();
return new PageResult<>(voList, pageResult.getTotal());
}
@Override
public OrderDetailVO getDetail(Long orderId) {
// 1. 查询主表
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
throw new RuntimeException("工单不存在: " + orderId);
}
// 2. 找到对应的扩展查询处理器
OrderExtQueryHandler handler = findExtQueryHandler(order.getOrderType());
// 3. 构建详情VO
if (handler != null) {
return handler.buildDetailVO(order);
} else {
// 没有对应的扩展查询处理器返回基础VO
return convertToDetailVO(order);
}
}
@Override
public Map<String, Object> getStats(String groupBy) {
Map<String, Object> result = new HashMap<>();
if ("status".equals(groupBy)) {
// 按类型和状态统计
List<OpsOrderDO> allOrders = opsOrderMapper.selectList();
Map<String, Map<String, Long>> typeStatusMap = new HashMap<>();
for (OpsOrderDO order : allOrders) {
String type = order.getOrderType();
String status = order.getStatus();
typeStatusMap.computeIfAbsent(type, k -> new HashMap<>())
.merge(status, 1L, Long::sum);
}
result.putAll(typeStatusMap);
} else {
// 只按类型统计
List<OpsOrderDO> allOrders = opsOrderMapper.selectList();
Map<String, Long> typeCountMap = new HashMap<>();
for (OpsOrderDO order : allOrders) {
String type = order.getOrderType();
typeCountMap.merge(type, 1L, Long::sum);
}
result.putAll(typeCountMap);
}
return result;
}
/**
* 转换为汇总VO并填充扩展信息
*/
private OrderSummaryVO convertToSummaryVO(OpsOrderDO order) {
OrderSummaryVO vo = OrderSummaryVO.builder()
.id(order.getId())
.orderCode(order.getOrderCode())
.orderType(order.getOrderType())
.title(order.getTitle())
.description(order.getDescription())
.priority(order.getPriority())
.status(order.getStatus())
.areaId(order.getAreaId())
.location(order.getLocation())
.assigneeId(order.getAssigneeId())
.startTime(order.getStartTime())
.endTime(order.getEndTime())
.createTime(order.getCreateTime())
.build();
// 填充扩展信息
OrderExtQueryHandler handler = findExtQueryHandler(order.getOrderType());
if (handler != null) {
handler.enrichWithExtInfo(vo, order.getId());
}
return vo;
}
/**
* 转换为详情VO基础版本不含扩展信息
*/
private OrderDetailVO convertToDetailVO(OpsOrderDO order) {
return 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();
}
/**
* 查找对应的扩展查询处理器
*/
private OrderExtQueryHandler findExtQueryHandler(String orderType) {
return extQueryHandlers.stream()
.filter(handler -> handler.supports(orderType))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,103 @@
package com.viewsh.module.ops.service;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 工单汇总信息 VO
* <p>
* 用于工单中心分页查询包含主表信息和扩展信息Map形式
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSummaryVO {
/**
* 工单ID
*/
private Long id;
/**
* 工单编号
*/
private String orderCode;
/**
* 工单类型CLEAN/SECURITY/FACILITIES/SERVICE
*/
private String orderType;
/**
* 工单标题
*/
private String title;
/**
* 工单描述
*/
private String description;
/**
* 优先级0=P0/1=P1/2=P2
*/
private Integer priority;
/**
* 工单状态
*/
private String status;
/**
* 区域ID
*/
private Long areaId;
/**
* 具体位置描述
*/
private String location;
/**
* 当前执行人ID
*/
private Long assigneeId;
/**
* 当前执行人姓名(冗余)
*/
private String assigneeName;
/**
* 工单开始时间
*/
private LocalDateTime startTime;
/**
* 工单结束时间
*/
private LocalDateTime endTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 扩展信息Map形式
* <p>
* 根据工单类型不同,包含不同的扩展字段:
* - CLEAN: expectedDuration, cleaningType, difficultyLevel
* - SECURITY: route, checkpoint, patrolTime
*/
@Builder.Default
private Map<String, Object> extInfo = new java.util.HashMap<>();
}

View File

@@ -0,0 +1,182 @@
package com.viewsh.module.ops.infrastructure.code;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 工单编号生成器测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
class OrderCodeGeneratorTest {
@Mock
private StringRedisTemplate stringRedisTemplate;
@Mock
private ValueOperations<String, String> valueOperations;
private OrderCodeGenerator orderCodeGenerator;
private static final String DATE_FORMAT = "yyyyMMdd";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);
@BeforeEach
void setUp() {
orderCodeGenerator = new OrderCodeGenerator();
ReflectionTestUtils.setField(orderCodeGenerator, "stringRedisTemplate", stringRedisTemplate);
// Mock stringRedisTemplate.opsForValue()
lenient().when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
}
@Test
void testGenerate_FirstOrderOfToday() {
// Given
when(valueOperations.increment(anyString())).thenReturn(1L);
// When
String orderCode = orderCodeGenerator.generate("CLEAN");
// Then
String expectedDate = LocalDate.now().format(DATE_FORMAT);
String expected = String.format("CLEAN-%s-0001", expectedDate);
assertEquals(expected, orderCode);
}
@Test
void testGenerate_SecondOrderOfToday() {
// Given
when(valueOperations.increment(anyString())).thenReturn(2L);
// When
String orderCode = orderCodeGenerator.generate("CLEAN");
// Then
String expectedDate = LocalDate.now().format(DATE_FORMAT);
String expected = String.format("CLEAN-%s-0002", expectedDate);
assertEquals(expected, orderCode);
}
@Test
void testGenerate_DifferentBusinessTypes() {
// Given
when(valueOperations.increment(anyString())).thenReturn(1L);
// When
String cleanCode = orderCodeGenerator.generate("CLEAN");
String securityCode = orderCodeGenerator.generate("SECURITY");
// Then
assertTrue(cleanCode.startsWith("CLEAN-"));
assertTrue(securityCode.startsWith("SECURITY-"));
assertNotEquals(cleanCode, securityCode);
}
@Test
void testGenerate_LargeSequenceNumber() {
// Given
when(valueOperations.increment(anyString())).thenReturn(9999L);
// When
String orderCode = orderCodeGenerator.generate("CLEAN");
// Then
String expectedDate = LocalDate.now().format(DATE_FORMAT);
String expected = String.format("CLEAN-%s-9999", expectedDate);
assertEquals(expected, orderCode);
}
@Test
void testGenerate_NullBusinessType_ThrowsException() {
// When & Then
assertThrows(IllegalArgumentException.class, () -> orderCodeGenerator.generate(null));
}
@Test
void testGenerate_EmptyBusinessType_ThrowsException() {
// When & Then
assertThrows(IllegalArgumentException.class, () -> orderCodeGenerator.generate(""));
}
@Test
void testGenerate_SetsExpirationOnFirstUse() {
// Given
when(valueOperations.increment(anyString())).thenReturn(1L);
// When
orderCodeGenerator.generate("CLEAN");
// Then
verify(stringRedisTemplate).expire(anyString(), eq(7L), any());
}
@Test
void testGetCurrentSeq_NoExistingRecords_ReturnsZero() {
// Given
when(valueOperations.get(anyString())).thenReturn(null);
// When
long seq = orderCodeGenerator.getCurrentSeq("CLEAN");
// Then
assertEquals(0, seq);
}
@Test
void testGetCurrentSeq_ExistingRecords_ReturnsValue() {
// Given
when(valueOperations.get(anyString())).thenReturn("5");
// When
long seq = orderCodeGenerator.getCurrentSeq("CLEAN");
// Then
assertEquals(5, seq);
}
@Test
void testReset_DeletesKey() {
// When
orderCodeGenerator.reset("CLEAN");
// Then
verify(stringRedisTemplate).delete(contains("CLEAN"));
}
@Test
void testGenerate_FormatConsistency() {
// Given
when(valueOperations.increment(anyString())).thenReturn(1L);
// When
String orderCode = orderCodeGenerator.generate("CLEAN");
// Then: 验证格式为 {TYPE}-{DATE}-{SEQUENCE}
String[] parts = orderCode.split("-");
assertEquals(3, parts.length);
assertEquals("CLEAN", parts[0]);
// 验证日期部分是8位数字
assertEquals(8, parts[1].length());
assertTrue(parts[1].matches("\\d{8}"));
// 验证序号部分是4位数字
assertEquals(4, parts[2].length());
assertTrue(parts[2].matches("\\d{4}"));
}
}

View File

@@ -0,0 +1,178 @@
package com.viewsh.module.ops.infrastructure.id;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
/**
* 雪花算法ID生成器测试
*
* @author lzh
*/
class SnowflakeIdGeneratorTest {
private SnowflakeIdGenerator snowflake;
@BeforeEach
void setUp() {
snowflake = new SnowflakeIdGenerator(1, 1);
}
@Test
void testNextId_ReturnsUniqueId() {
// When
Long id1 = snowflake.nextId();
Long id2 = snowflake.nextId();
// Then
assertNotNull(id1);
assertNotNull(id2);
assertNotEquals(id1, id2);
assertTrue(id2 > id1, "ID应该递增");
}
@Test
void testNextId_GeneratesIncreasingIds() {
// Given
Long previousId = 0L;
// When: 生成100个ID
for (int i = 0; i < 100; i++) {
Long currentId = snowflake.nextId();
// Then
assertTrue(currentId > previousId, "ID应该严格递增");
previousId = currentId;
}
}
@Test
void testConstructor_ValidDatacenterId_Success() {
// When & Then: 测试边界值
assertDoesNotThrow(() -> new SnowflakeIdGenerator(0, 0));
assertDoesNotThrow(() -> new SnowflakeIdGenerator(31, 31));
}
@Test
void testConstructor_InvalidDatacenterId_ThrowsException() {
// When & Then
assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(-1, 1));
assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(32, 1));
}
@Test
void testConstructor_InvalidMachineId_ThrowsException() {
// When & Then
assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(1, -1));
assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(1, 32));
}
@Test
void testNextId_DifferentDatacenterOrMachine_GeneratesDifferentIds() {
// Given
SnowflakeIdGenerator generator1 = new SnowflakeIdGenerator(1, 1);
SnowflakeIdGenerator generator2 = new SnowflakeIdGenerator(1, 2);
SnowflakeIdGenerator generator3 = new SnowflakeIdGenerator(2, 1);
// When
Long id1 = generator1.nextId();
Long id2 = generator2.nextId();
Long id3 = generator3.nextId();
// Then: 不同数据中心或机器生成的ID应该不同
assertNotEquals(id1, id2);
assertNotEquals(id1, id3);
assertNotEquals(id2, id3);
}
@Test
void testNextId_Concurrent_ThreadSafe() throws InterruptedException {
// Given
int threadCount = 10;
int idsPerThread = 1000;
Set<Long> allIds = Collections.newSetFromMap(new ConcurrentHashMap<>());
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
// When: 多线程并发生成ID
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
for (int j = 0; j < idsPerThread; j++) {
Long id = snowflake.nextId();
boolean isNew = allIds.add(id);
assertTrue(isNew, "并发生成ID应该唯一");
}
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
// Then: 验证ID唯一性
assertEquals(threadCount * idsPerThread, allIds.size());
}
@RepeatedTest(10)
void testNextId_HighThroughput_NoDuplicateInSameMillisecond() {
// Given
Set<Long> ids = new HashSet<>();
// When: 快速生成100个ID
for (int i = 0; i < 100; i++) {
ids.add(snowflake.nextId());
}
// Then: 同一毫秒内应该生成不同的ID
assertEquals(100, ids.size());
}
@Test
void testParseTimestamp() {
// Given
Long id = snowflake.nextId();
long beforeParse = System.currentTimeMillis();
// When
long timestamp = SnowflakeIdGenerator.parseTimestamp(id);
long afterParse = System.currentTimeMillis();
// Then: 解析的时间戳应该接近当前时间
assertTrue(timestamp >= beforeParse - 1000, "时间戳应该接近当前时间");
assertTrue(timestamp <= afterParse + 1000, "时间戳应该接近当前时间");
}
@Test
void testParseDatacenterId() {
// Given
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(5, 10);
Long id = generator.nextId();
// When
long datacenterId = SnowflakeIdGenerator.parseDatacenterId(id);
// Then
assertEquals(5, datacenterId);
}
@Test
void testParseMachineId() {
// Given
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(5, 10);
Long id = generator.nextId();
// When
long machineId = SnowflakeIdGenerator.parseMachineId(id);
// Then
assertEquals(10, machineId);
}
}

View File

@@ -0,0 +1,93 @@
package com.viewsh.module.ops.controller.admin;
import com.viewsh.framework.common.pojo.CommonResult;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.module.ops.service.OrderDetailVO;
import com.viewsh.module.ops.service.OrderQueryService;
import com.viewsh.module.ops.service.OrderSummaryVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import static com.viewsh.framework.common.pojo.CommonResult.success;
/**
* 管理后台 - 工单中心 Controller
* <p>
* 工单中心提供综合查询能力,支持所有业务类型的工单查询
*
* @author lzh
*/
@Tag(name = "管理后台 - 工单中心")
@Slf4j
@RestController
@RequestMapping("/ops/order-center")
@Validated
public class OrderCenterController {
@Resource
private OrderQueryService orderQueryService;
@GetMapping("/page")
@Operation(summary = "工单中心分页查询")
@Parameter(name = "page", description = "页码", required = false)
@Parameter(name = "size", description = "每页大小", required = false)
@Parameter(name = "orderType", description = "工单类型", required = false)
@Parameter(name = "status", description = "工单状态", required = false)
@Parameter(name = "priority", description = "优先级", required = false)
@Parameter(name = "areaId", description = "区域ID", required = false)
@Parameter(name = "assigneeId", description = "执行人ID", required = false)
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
public CommonResult<PageResult<OrderSummaryVO>> queryPage(
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
@RequestParam(value = "size", required = false, defaultValue = "20") Integer size,
@RequestParam(value = "orderType", required = false) String orderType,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "priority", required = false) Integer priority,
@RequestParam(value = "areaId", required = false) Long areaId,
@RequestParam(value = "assigneeId", required = false) Long assigneeId,
@RequestParam(value = "orderCode", required = false) String orderCode,
@RequestParam(value = "title", required = false) String title) {
OrderQueryService.OrderQuery query = OrderQueryService.OrderQuery.builder()
.withPage(page)
.withSize(size)
.withOrderType(orderType)
.withStatus(status)
.withPriority(priority)
.withAreaId(areaId)
.withAssigneeId(assigneeId)
.withOrderCode(orderCode)
.withTitle(title);
PageResult<OrderSummaryVO> result = orderQueryService.queryPage(query);
return success(result);
}
@GetMapping("/detail/{id}")
@Operation(summary = "工单详情查询")
@Parameter(name = "id", description = "工单ID", required = true)
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
public CommonResult<OrderDetailVO> getDetail(@PathVariable("id") Long id) {
OrderDetailVO detail = orderQueryService.getDetail(id);
return success(detail);
}
@GetMapping("/stats")
@Operation(summary = "工单统计")
@Parameter(name = "groupBy", description = "分组维度status按状态统计", required = false)
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
public CommonResult<Map<String, Object>> getStats(
@RequestParam(value = "groupBy", required = false) String groupBy) {
Map<String, Object> stats = orderQueryService.getStats(groupBy);
return success(stats);
}
}

View File

@@ -140,4 +140,11 @@ viewsh:
tenant: # 多租户相关配置项
enable: true
--- #################### 工单相关配置 ####################
ops:
order:
datacenter-id: 1 # 数据中心ID0-31
machine-id: 1 # 机器ID0-31
debug: false

View File

@@ -0,0 +1,201 @@
package com.viewsh.module.ops.controller.admin;
import com.viewsh.framework.common.pojo.CommonResult;
import com.viewsh.framework.common.pojo.PageResult;
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 com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.service.OrderDetailVO;
import com.viewsh.module.ops.service.OrderQueryService;
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 org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* 工单中心控制器测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
class OrderCenterControllerTest {
@Mock
private OrderQueryService orderQueryService;
@InjectMocks
private OrderCenterController controller;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
@Test
void testQueryPage_Success() {
// Given
OrderSummaryVO vo = OrderSummaryVO.builder()
.id(1L)
.orderCode("CLEAN-20250119-0001")
.orderType("CLEAN")
.title("2楼电梯厅保洁")
.status(WorkOrderStatusEnum.PENDING.getStatus())
.priority(PriorityEnum.P2.getPriority())
.areaId(100L)
.createTime(LocalDateTime.now())
.extInfo(new HashMap<>())
.build();
PageResult<OrderSummaryVO> pageResult = new PageResult<>(List.of(vo), 1L);
when(orderQueryService.queryPage(any())).thenReturn(pageResult);
// When
CommonResult<PageResult<OrderSummaryVO>> result = controller.queryPage(
1, 20, "CLEAN", null, null, null, null, null, null);
// Then
assertTrue(result.isSuccess());
assertNotNull(result.getData());
assertEquals(1, result.getData().getList().size());
assertEquals("CLEAN-20250119-0001", result.getData().getList().get(0).getOrderCode());
verify(orderQueryService, times(1)).queryPage(any());
}
@Test
void testQueryPage_WithMultipleFilters() {
// Given
PageResult<OrderSummaryVO> emptyResult = new PageResult<>(List.of(), 0L);
when(orderQueryService.queryPage(any())).thenReturn(emptyResult);
// When
controller.queryPage(1, 20, "CLEAN", "PENDING",
PriorityEnum.P2.getPriority(), 100L, null, null, null);
// Then
verify(orderQueryService, times(1)).queryPage(argThat(query ->
query.getOrderType().equals("CLEAN") &&
query.getStatus().equals("PENDING") &&
query.getPriority().equals(PriorityEnum.P2.getPriority()) &&
query.getAreaId().equals(100L)
));
}
@Test
void testGetDetail_Success() {
// Given
OrderDetailVO vo = OrderDetailVO.builder()
.id(1L)
.orderCode("CLEAN-20250119-0001")
.orderType("CLEAN")
.title("2楼电梯厅保洁")
.status(WorkOrderStatusEnum.PENDING.getStatus())
.priority(PriorityEnum.P2.getPriority())
.areaId(100L)
.extInfo(new HashMap<>())
.build();
when(orderQueryService.getDetail(1L)).thenReturn(vo);
// When
CommonResult<OrderDetailVO> result = controller.getDetail(1L);
// Then
assertTrue(result.isSuccess());
assertNotNull(result.getData());
assertEquals(1L, result.getData().getId());
assertEquals("CLEAN-20250119-0001", result.getData().getOrderCode());
verify(orderQueryService, times(1)).getDetail(1L);
}
@Test
void testGetDetail_NotFound_ThrowsException() {
// Given
when(orderQueryService.getDetail(999L))
.thenThrow(new RuntimeException("工单不存在"));
// When & Then
assertThrows(RuntimeException.class, () -> controller.getDetail(999L));
verify(orderQueryService, times(1)).getDetail(999L);
}
@Test
void testGetStats_Success() {
// Given
Map<String, Object> stats = new HashMap<>();
stats.put("CLEAN", 50L);
stats.put("SECURITY", 30L);
when(orderQueryService.getStats(null)).thenReturn(stats);
// When
CommonResult<Map<String, Object>> result = controller.getStats(null);
// Then
assertTrue(result.isSuccess());
assertNotNull(result.getData());
assertEquals(50L, result.getData().get("CLEAN"));
assertEquals(30L, result.getData().get("SECURITY"));
verify(orderQueryService, times(1)).getStats(null);
}
@Test
void testGetStats_WithGroupByStatus() {
// Given
Map<String, Object> stats = new HashMap<>();
Map<String, Long> cleanStatus = new HashMap<>();
cleanStatus.put("PENDING", 10L);
cleanStatus.put("COMPLETED", 20L);
stats.put("CLEAN", cleanStatus);
when(orderQueryService.getStats("status")).thenReturn(stats);
// When
CommonResult<Map<String, Object>> result = controller.getStats("status");
// Then
assertTrue(result.isSuccess());
assertNotNull(result.getData());
assertNotNull(result.getData().get("CLEAN"));
verify(orderQueryService, times(1)).getStats("status");
}
@Test
void testQueryPage_DefaultPageAndSize() {
// Given
PageResult<OrderSummaryVO> emptyResult = new PageResult<>(List.of(), 0L);
when(orderQueryService.queryPage(any())).thenReturn(emptyResult);
// When: 不传分页参数
controller.queryPage(null, null, null, null, null, null, null, null, null);
// Then: 应该使用默认值
verify(orderQueryService, times(1)).queryPage(argThat(query ->
query.getPage() == 1 && query.getSize() == 20
));
}
}