feat(ops): refactor-order-operations
This commit is contained in:
@@ -30,4 +30,18 @@ public class CleanOrderAutoCreateReqDTO extends OpsOrderCreateReqDTO {
|
|||||||
@Min(value = 1, message = "难度等级必须大于0")
|
@Min(value = 1, message = "难度等级必须大于0")
|
||||||
private Integer difficultyLevel;
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package com.viewsh.module.ops.environment.integration.consumer;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
||||||
import com.viewsh.module.iot.api.device.dto.ResetTrafficCounterReqDTO;
|
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.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.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 jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
||||||
@@ -56,7 +55,7 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
|
|||||||
private StringRedisTemplate stringRedisTemplate;
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private OpsOrderService opsOrderService;
|
private CleanOrderService cleanOrderService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private IotDeviceControlApi iotDeviceControlApi;
|
private IotDeviceControlApi iotDeviceControlApi;
|
||||||
@@ -93,28 +92,33 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
|
|||||||
log.info("[CleanOrderCreateEventHandler] 收到工单创建事件: eventId={}, areaId={}, triggerSource={}",
|
log.info("[CleanOrderCreateEventHandler] 收到工单创建事件: eventId={}, areaId={}, triggerSource={}",
|
||||||
event.getEventId(), event.getAreaId(), event.getTriggerSource());
|
event.getEventId(), event.getAreaId(), event.getTriggerSource());
|
||||||
|
|
||||||
// 1. 构建创建请求
|
// 1. 构建创建请求(使用 CleanOrderAutoCreateReqDTO)
|
||||||
OpsOrderCreateReqDTO createReq = new OpsOrderCreateReqDTO();
|
CleanOrderAutoCreateReqDTO createReq = new CleanOrderAutoCreateReqDTO();
|
||||||
createReq.setOrderType(event.getOrderType());
|
createReq.setOrderType("CLEAN");
|
||||||
createReq.setSourceType(SourceTypeEnum.TRAFFIC.getType()); // 系统触发
|
createReq.setSourceType("TRAFFIC"); // 系统触发
|
||||||
createReq.setTitle(generateOrderTitle(event));
|
createReq.setTitle(generateOrderTitle(event));
|
||||||
createReq.setDescription(generateOrderDescription(event));
|
createReq.setDescription(generateOrderDescription(event));
|
||||||
createReq.setPriority(PriorityEnum.fromPriority(event.getPriority() != null ?
|
createReq.setPriority(PriorityEnum.fromPriority(event.getPriority() != null ?
|
||||||
Integer.parseInt(event.getPriority()) : 2).getPriority());
|
Integer.parseInt(event.getPriority()) : 2).getPriority());
|
||||||
createReq.setAreaId(event.getAreaId());
|
createReq.setAreaId(event.getAreaId());
|
||||||
|
// location 字段由 areaId 自动关联,不需要在事件中传递
|
||||||
|
|
||||||
// 2. 创建工单
|
// 扩展字段
|
||||||
Long orderId = opsOrderService.createOrder(createReq);
|
createReq.setExpectedDuration(calculateExpectedDuration(event));
|
||||||
|
createReq.setCleaningType("ROUTINE"); // 可根据triggerSource动态设置
|
||||||
|
createReq.setDifficultyLevel(3);
|
||||||
|
|
||||||
// 3. 更新工单的触发信息(集成字段)
|
// IoT集成字段
|
||||||
opsOrderService.updateIntegrationFields(
|
createReq.setTriggerSource(event.getTriggerSource());
|
||||||
orderId,
|
createReq.setTriggerRuleId(extractRuleId(event));
|
||||||
event.getTriggerSource(),
|
createReq.setTriggerDeviceId(event.getTriggerDeviceId());
|
||||||
event.getTriggerDeviceId(),
|
createReq.setTriggerDeviceKey(event.getTriggerDeviceKey());
|
||||||
event.getTriggerDeviceKey()
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. 如果是客流触发的工单,重置客流计数器基准值
|
// 2. 创建工单(同时创建主表+扩展表)
|
||||||
|
Long orderId = cleanOrderService.createAutoCleanOrder(createReq);
|
||||||
|
|
||||||
|
// 3. 如果是客流触发的工单,重置客流计数器基准值
|
||||||
|
// TODO: 需要优化这个工单是否创建成功,才重置
|
||||||
if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) {
|
if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) {
|
||||||
resetTrafficCounter(event, orderId);
|
resetTrafficCounter(event, orderId);
|
||||||
}
|
}
|
||||||
@@ -181,6 +185,8 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
|
|||||||
return "客流阈值触发保洁";
|
return "客流阈值触发保洁";
|
||||||
} else if ("IOT_BEACON".equals(event.getTriggerSource())) {
|
} else if ("IOT_BEACON".equals(event.getTriggerSource())) {
|
||||||
return "信标检测触发保洁";
|
return "信标检测触发保洁";
|
||||||
|
} else if ("IOT_SIGNAL_LOSS".equals(event.getTriggerSource())) {
|
||||||
|
return "离线超时触发保洁";
|
||||||
} else {
|
} else {
|
||||||
return "IoT设备触发保洁";
|
return "IoT设备触发保洁";
|
||||||
}
|
}
|
||||||
@@ -205,4 +211,39 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
|
|||||||
|
|
||||||
return desc.toString();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: 更新工单状态为已暂停
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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) {
|
|
||||||
// // 触发保洁工单
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
|
||||||
// 不抛出异常,避免影响主流程
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqD
|
|||||||
* <p>
|
* <p>
|
||||||
* 变更说明:
|
* 变更说明:
|
||||||
* - 移除了暂停/恢复/打断方法(由 {@link com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager} 处理)
|
* - 移除了暂停/恢复/打断方法(由 {@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
|
* @author lzh
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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.CleanOrderAutoCreateReqDTO;
|
||||||
import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO;
|
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.dal.mysql.workorder.OpsOrderCleanExtMapper;
|
||||||
import com.viewsh.module.ops.environment.handler.CleanOrderEventHandler;
|
import com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener;
|
||||||
import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService;
|
import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
|
||||||
import com.viewsh.module.ops.environment.service.dispatch.CleanerAreaAssignStrategy;
|
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
|
||||||
import com.viewsh.module.ops.service.order.OpsOrderService;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -30,23 +29,19 @@ import java.time.Duration;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保洁工单服务实现(简化版)
|
* 保洁工单服务实现(重构版)
|
||||||
* <p>
|
* <p>
|
||||||
* 职责:
|
* 职责:
|
||||||
* 1. 工单创建与自动分配
|
* 1. 工单创建与自动分配(直接操作主表+扩展表)
|
||||||
* 2. 保洁特有的状态转换编排(confirm, arrive, complete)
|
* 2. 保洁特有的状态转换编排(confirm, arrive, complete)
|
||||||
* 3. 作业时长计算
|
* 3. 作业时长计算
|
||||||
* <p>
|
* <p>
|
||||||
* 架构说明:
|
* 架构说明:
|
||||||
* - 状态同步:委托给 {@link OrderLifecycleManager}
|
* - 直接操作主表和扩展表,不依赖 OpsOrderService
|
||||||
* - 派单决策:使用新的 {@link DispatchEngine}
|
* - 使用公用组件 OrderIdGenerator、OrderCodeGenerator
|
||||||
* - 保洁员状态:由 {@link com.viewsh.module.ops.environment.integration.listener.CleanerStateChangeListener} 处理
|
* - 使用事件发布器 OrderEventPublisher
|
||||||
* - 通知逻辑:由 {@link CleanOrderEventHandler} 处理
|
* - 状态同步委托给 OrderLifecycleManager
|
||||||
* <p>
|
* - 派单决策使用 DispatchEngine
|
||||||
* 变更历史:
|
|
||||||
* - Phase 3: 使用新的调度引擎和生命周期管理器
|
|
||||||
* - Phase 3: 移除直接的状态管理代码(改为事件驱动)
|
|
||||||
* - Phase 3: 瘦身代码,专注保洁业务编排
|
|
||||||
*
|
*
|
||||||
* @author lzh
|
* @author lzh
|
||||||
*/
|
*/
|
||||||
@@ -54,9 +49,6 @@ import java.time.LocalDateTime;
|
|||||||
@Service
|
@Service
|
||||||
public class CleanOrderServiceImpl implements CleanOrderService {
|
public class CleanOrderServiceImpl implements CleanOrderService {
|
||||||
|
|
||||||
@Resource
|
|
||||||
private OpsOrderService opsOrderService;
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private OpsOrderMapper opsOrderMapper;
|
private OpsOrderMapper opsOrderMapper;
|
||||||
|
|
||||||
@@ -64,13 +56,16 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
private OpsOrderCleanExtMapper cleanExtMapper;
|
private OpsOrderCleanExtMapper cleanExtMapper;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private OrderQueueService orderQueueService;
|
private OrderIdGenerator orderIdGenerator;
|
||||||
|
|
||||||
// @Resource
|
@Resource
|
||||||
// private CleanerAreaAssignStrategy cleanerAreaAssignStrategy;
|
private OrderCodeGenerator orderCodeGenerator;
|
||||||
//
|
|
||||||
// @Resource
|
@Resource
|
||||||
// private CleanerStatusService cleanerStatusService;
|
private OrderEventPublisher orderEventPublisher;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OrderQueueService orderQueueService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private DispatchEngine dispatchEngine;
|
private DispatchEngine dispatchEngine;
|
||||||
@@ -79,20 +74,40 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
private OrderLifecycleManager orderLifecycleManager;
|
private OrderLifecycleManager orderLifecycleManager;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private CleanOrderEventHandler cleanOrderEventHandler;
|
private CleanOrderEventListener cleanOrderEventListener;
|
||||||
|
|
||||||
@Resource
|
|
||||||
private OrderEventPublisher orderEventPublisher;
|
|
||||||
|
|
||||||
// ==================== 工单创建 ====================
|
// ==================== 工单创建 ====================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Long createAutoCleanOrder(CleanOrderAutoCreateReqDTO createReq) {
|
public Long createAutoCleanOrder(CleanOrderAutoCreateReqDTO createReq) {
|
||||||
// 1. 调用基础服务创建工单
|
// 1. 生成ID和编号
|
||||||
Long orderId = opsOrderService.createOrder(createReq);
|
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()
|
OpsOrderCleanExtDO cleanExt = OpsOrderCleanExtDO.builder()
|
||||||
.opsOrderId(orderId)
|
.opsOrderId(orderId)
|
||||||
.isAuto(1)
|
.isAuto(1)
|
||||||
@@ -102,62 +117,29 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
.build();
|
.build();
|
||||||
cleanExtMapper.insert(cleanExt);
|
cleanExtMapper.insert(cleanExt);
|
||||||
|
|
||||||
log.info("创建自动保洁工单成功: orderId={}, expectedDuration={}分钟",
|
log.info("创建自动保洁工单成功: orderId={}, orderCode={}, expectedDuration={}分钟, triggerSource={}",
|
||||||
orderId, createReq.getExpectedDuration());
|
orderId, orderCode, createReq.getExpectedDuration(), createReq.getTriggerSource());
|
||||||
|
|
||||||
// 3. 发布工单创建事件,由 OrderCreatedEventListener 触发调度
|
// 5. 发布工单创建事件,由 CleanOrderEventListener 触发调度
|
||||||
OrderCreatedEvent event = OrderCreatedEvent.builder()
|
OrderCreatedEvent event = OrderCreatedEvent.builder()
|
||||||
.orderId(orderId)
|
.orderId(orderId)
|
||||||
.orderType("CLEAN")
|
.orderType("CLEAN")
|
||||||
|
.orderCode(orderCode)
|
||||||
|
.title(createReq.getTitle())
|
||||||
.areaId(createReq.getAreaId())
|
.areaId(createReq.getAreaId())
|
||||||
.priority(PriorityEnum.fromPriority(createReq.getPriority()).getPriority())
|
.priority(order.getPriority())
|
||||||
.createTime(java.time.LocalDateTime.now())
|
.createTime(LocalDateTime.now())
|
||||||
.build()
|
.build()
|
||||||
.addPayload("isAuto", true)
|
.addPayload("isAuto", true)
|
||||||
.addPayload("expectedDuration", createReq.getExpectedDuration());
|
.addPayload("expectedDuration", createReq.getExpectedDuration())
|
||||||
|
.addPayload("triggerSource", createReq.getTriggerSource());
|
||||||
|
|
||||||
orderEventPublisher.publishOrderCreated(event);
|
orderEventPublisher.publishOrderCreated(event);
|
||||||
|
|
||||||
return orderId;
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -210,7 +192,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 语音播报
|
// 语音播报
|
||||||
cleanOrderEventHandler.sendNewOrderNotification(cleanerId, orderId);
|
cleanOrderEventListener.sendNewOrderNotification(cleanerId, orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -272,7 +254,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
DispatchResult result = dispatchEngine.urgentInterrupt(orderId, queueDTO.getUserId());
|
DispatchResult result = dispatchEngine.urgentInterrupt(orderId, queueDTO.getUserId());
|
||||||
|
|
||||||
// 6. 发送优先级升级通知
|
// 6. 发送优先级升级通知
|
||||||
cleanOrderEventHandler.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode());
|
cleanOrderEventListener.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode());
|
||||||
|
|
||||||
return result.isSuccess();
|
return result.isSuccess();
|
||||||
}
|
}
|
||||||
@@ -298,7 +280,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
orderLifecycleManager.transition(request);
|
orderLifecycleManager.transition(request);
|
||||||
// 注意:保洁员状态更新由 CleanerStateChangeListener 处理
|
// 注意:保洁员状态更新由 CleanOrderEventListener 处理
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -362,7 +344,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
log.info("已自动推送下一个任务: cleanerId={}", cleanerId);
|
log.info("已自动推送下一个任务: cleanerId={}", cleanerId);
|
||||||
} else {
|
} else {
|
||||||
log.info("无等待任务,保洁员变空闲: cleanerId={}", cleanerId);
|
log.info("无等待任务,保洁员变空闲: cleanerId={}", cleanerId);
|
||||||
// 状态更新由 CleanerStateChangeListener 处理
|
// 状态更新由 CleanOrderEventListener 处理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,17 +352,17 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playVoiceForNewOrder(Long cleanerId) {
|
public void playVoiceForNewOrder(Long cleanerId) {
|
||||||
cleanOrderEventHandler.sendNewOrderNotification(cleanerId, null);
|
cleanOrderEventListener.sendNewOrderNotification(cleanerId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playVoiceForQueuedOrder(Long cleanerId, int queueCount) {
|
public void playVoiceForQueuedOrder(Long cleanerId, int queueCount) {
|
||||||
cleanOrderEventHandler.sendQueuedOrderNotification(cleanerId, queueCount);
|
cleanOrderEventListener.sendQueuedOrderNotification(cleanerId, queueCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playVoiceForNextTask(Long cleanerId, int queueCount, String nextTaskTitle) {
|
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) {
|
private void recordArrivedTime(Long orderId) {
|
||||||
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);
|
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);
|
||||||
@@ -418,7 +399,6 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录完成时间
|
* 记录完成时间
|
||||||
* 注意:此方法在被 @Transactional 的公共方法调用时,会参与同一事务
|
|
||||||
*/
|
*/
|
||||||
private void recordCompletedTime(Long orderId) {
|
private void recordCompletedTime(Long orderId) {
|
||||||
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);
|
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);
|
||||||
|
|||||||
@@ -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.OrderQueueDTO;
|
||||||
import com.viewsh.module.ops.api.queue.OrderQueueService;
|
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.AssigneeStatus;
|
||||||
import com.viewsh.module.ops.core.dispatch.model.DispatchDecision;
|
import com.viewsh.module.ops.core.dispatch.model.DispatchDecision;
|
||||||
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
|
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
|
||||||
@@ -46,7 +47,7 @@ public class CleanerPriorityScheduleStrategy implements ScheduleStrategy {
|
|||||||
private OrderQueueService orderQueueService;
|
private OrderQueueService orderQueueService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private com.viewsh.module.ops.core.dispatch.DispatchEngine dispatchEngine;
|
private DispatchEngine dispatchEngine;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,10 +58,10 @@ public interface OrderLifecycleManager {
|
|||||||
OrderTransitionResult enqueue(OrderTransitionRequest request);
|
OrderTransitionResult enqueue(OrderTransitionRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工单出队并派单:QUEUED → DISPATCHED
|
* 工单出队并派单:PENDING/QUEUED → DISPATCHED
|
||||||
* <p>
|
* <p>
|
||||||
* 状态转换:
|
* 状态转换:
|
||||||
* - 工单状态:QUEUED → DISPATCHED
|
* - 工单状态:PENDING/QUEUED → DISPATCHED
|
||||||
* - 队列状态:WAITING → PROCESSING
|
* - 队列状态:WAITING → PROCESSING
|
||||||
*
|
*
|
||||||
* @param request 状态转换请求
|
* @param request 状态转换请求
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据中心ID(0-31)
|
||||||
|
* 默认值:1
|
||||||
|
*/
|
||||||
|
private int datacenterId = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机器ID(0-31)
|
||||||
|
* 默认值:1
|
||||||
|
*/
|
||||||
|
private int machineId = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package com.viewsh.module.ops.infrastructure.id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 雪花算法ID生成器
|
||||||
|
* <p>
|
||||||
|
* ID<49><44><EFBFBD>构(64位Long):
|
||||||
|
* - 1位符号位(永远为0)
|
||||||
|
* - 41位时间戳(毫秒级,可用69年)
|
||||||
|
* - 5位数据中心ID(0-31)
|
||||||
|
* - 5位机器ID(0-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 数据中心ID(0-31)
|
||||||
|
* @param machineId 机器ID(0-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<>();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<>();
|
||||||
|
}
|
||||||
@@ -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}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -140,4 +140,11 @@ viewsh:
|
|||||||
tenant: # 多租户相关配置项
|
tenant: # 多租户相关配置项
|
||||||
enable: true
|
enable: true
|
||||||
|
|
||||||
|
--- #################### 工单相关配置 ####################
|
||||||
|
|
||||||
|
ops:
|
||||||
|
order:
|
||||||
|
datacenter-id: 1 # 数据中心ID(0-31)
|
||||||
|
machine-id: 1 # 机器ID(0-31)
|
||||||
|
|
||||||
debug: false
|
debug: false
|
||||||
|
|||||||
@@ -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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user