feat(ops): refactor-order-operations
This commit is contained in:
@@ -30,4 +30,18 @@ public class CleanOrderAutoCreateReqDTO extends OpsOrderCreateReqDTO {
|
||||
@Min(value = 1, message = "难度等级必须大于0")
|
||||
private Integer difficultyLevel;
|
||||
|
||||
// ==================== 集成字段(IoT触发相关)====================
|
||||
|
||||
@Schema(description = "触发来源(IOT_TRAFFIC=客流阈值/IOT_BEACON=蓝牙信标/IOT_SIGNAL_LOSS=信号丢失超时)")
|
||||
private String triggerSource;
|
||||
|
||||
@Schema(description = "触发规则ID(关联 ops_area_device_relation.id)")
|
||||
private Long triggerRuleId;
|
||||
|
||||
@Schema(description = "触发设备ID(关联 iot_device.id)")
|
||||
private Long triggerDeviceId;
|
||||
|
||||
@Schema(description = "触发设备Key(冗余,便于查询)")
|
||||
private String triggerDeviceKey;
|
||||
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package com.viewsh.module.ops.environment.integration.consumer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
||||
import com.viewsh.module.iot.api.device.dto.ResetTrafficCounterReqDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.dto.OpsOrderCreateReqDTO;
|
||||
import com.viewsh.module.ops.enums.PriorityEnum;
|
||||
import com.viewsh.module.ops.enums.SourceTypeEnum;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO;
|
||||
import com.viewsh.module.ops.environment.integration.dto.CleanOrderCreateEventDTO;
|
||||
import com.viewsh.module.ops.service.order.OpsOrderService;
|
||||
import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
||||
@@ -56,7 +55,7 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Resource
|
||||
private OpsOrderService opsOrderService;
|
||||
private CleanOrderService cleanOrderService;
|
||||
|
||||
@Resource
|
||||
private IotDeviceControlApi iotDeviceControlApi;
|
||||
@@ -93,28 +92,33 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
|
||||
log.info("[CleanOrderCreateEventHandler] 收到工单创建事件: eventId={}, areaId={}, triggerSource={}",
|
||||
event.getEventId(), event.getAreaId(), event.getTriggerSource());
|
||||
|
||||
// 1. 构建创建请求
|
||||
OpsOrderCreateReqDTO createReq = new OpsOrderCreateReqDTO();
|
||||
createReq.setOrderType(event.getOrderType());
|
||||
createReq.setSourceType(SourceTypeEnum.TRAFFIC.getType()); // 系统触发
|
||||
// 1. 构建创建请求(使用 CleanOrderAutoCreateReqDTO)
|
||||
CleanOrderAutoCreateReqDTO createReq = new CleanOrderAutoCreateReqDTO();
|
||||
createReq.setOrderType("CLEAN");
|
||||
createReq.setSourceType("TRAFFIC"); // 系统触发
|
||||
createReq.setTitle(generateOrderTitle(event));
|
||||
createReq.setDescription(generateOrderDescription(event));
|
||||
createReq.setPriority(PriorityEnum.fromPriority(event.getPriority() != null ?
|
||||
Integer.parseInt(event.getPriority()) : 2).getPriority());
|
||||
createReq.setAreaId(event.getAreaId());
|
||||
// location 字段由 areaId 自动关联,不需要在事件中传递
|
||||
|
||||
// 2. 创建工单
|
||||
Long orderId = opsOrderService.createOrder(createReq);
|
||||
// 扩展字段
|
||||
createReq.setExpectedDuration(calculateExpectedDuration(event));
|
||||
createReq.setCleaningType("ROUTINE"); // 可根据triggerSource动态设置
|
||||
createReq.setDifficultyLevel(3);
|
||||
|
||||
// 3. 更新工单的触发信息(集成字段)
|
||||
opsOrderService.updateIntegrationFields(
|
||||
orderId,
|
||||
event.getTriggerSource(),
|
||||
event.getTriggerDeviceId(),
|
||||
event.getTriggerDeviceKey()
|
||||
);
|
||||
// IoT集成字段
|
||||
createReq.setTriggerSource(event.getTriggerSource());
|
||||
createReq.setTriggerRuleId(extractRuleId(event));
|
||||
createReq.setTriggerDeviceId(event.getTriggerDeviceId());
|
||||
createReq.setTriggerDeviceKey(event.getTriggerDeviceKey());
|
||||
|
||||
// 4. 如果是客流触发的工单,重置客流计数器基准值
|
||||
// 2. 创建工单(同时创建主表+扩展表)
|
||||
Long orderId = cleanOrderService.createAutoCleanOrder(createReq);
|
||||
|
||||
// 3. 如果是客流触发的工单,重置客流计数器基准值
|
||||
// TODO: 需要优化这个工单是否创建成功,才重置
|
||||
if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) {
|
||||
resetTrafficCounter(event, orderId);
|
||||
}
|
||||
@@ -181,6 +185,8 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
|
||||
return "客流阈值触发保洁";
|
||||
} else if ("IOT_BEACON".equals(event.getTriggerSource())) {
|
||||
return "信标检测触发保洁";
|
||||
} else if ("IOT_SIGNAL_LOSS".equals(event.getTriggerSource())) {
|
||||
return "离线超时触发保洁";
|
||||
} else {
|
||||
return "IoT设备触发保洁";
|
||||
}
|
||||
@@ -205,4 +211,39 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
|
||||
|
||||
return desc.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算预计作业时长(分钟)
|
||||
*/
|
||||
private Integer calculateExpectedDuration(CleanOrderCreateEventDTO event) {
|
||||
// 根据触发来源和客流数据估算时长
|
||||
if ("IOT_TRAFFIC".equals(event.getTriggerSource()) && event.getTriggerData() != null) {
|
||||
Object actualCountObj = event.getTriggerData().get("actualCount");
|
||||
if (actualCountObj != null) {
|
||||
Long actualCount = ((Number) actualCountObj).longValue();
|
||||
// 客流越大,预计耗时越长
|
||||
if (actualCount > 50) {
|
||||
return 45; // 高客流
|
||||
} else if (actualCount > 20) {
|
||||
return 30; // 中客流
|
||||
} else {
|
||||
return 20; // 低客流
|
||||
}
|
||||
}
|
||||
}
|
||||
return 30; // 默认30分钟
|
||||
}
|
||||
|
||||
/**
|
||||
* 从事件数据中提取规则ID
|
||||
*/
|
||||
private Long extractRuleId(CleanOrderCreateEventDTO event) {
|
||||
if (event.getTriggerData() != null && event.getTriggerData().containsKey("ruleId")) {
|
||||
Object ruleIdObj = event.getTriggerData().get("ruleId");
|
||||
if (ruleIdObj instanceof Number) {
|
||||
return ((Number) ruleIdObj).longValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
* 变更说明:
|
||||
* - 移除了暂停/恢复/打断方法(由 {@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
|
||||
*/
|
||||
|
||||
@@ -17,10 +17,9 @@ import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO;
|
||||
import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper;
|
||||
import com.viewsh.module.ops.environment.handler.CleanOrderEventHandler;
|
||||
import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService;
|
||||
import com.viewsh.module.ops.environment.service.dispatch.CleanerAreaAssignStrategy;
|
||||
import com.viewsh.module.ops.service.order.OpsOrderService;
|
||||
import com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener;
|
||||
import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
|
||||
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -30,23 +29,19 @@ import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 保洁工单服务实现(简化版)
|
||||
* 保洁工单服务实现(重构版)
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 工单创建与自动分配
|
||||
* 1. 工单创建与自动分配(直接操作主表+扩展表)
|
||||
* 2. 保洁特有的状态转换编排(confirm, arrive, complete)
|
||||
* 3. 作业时长计算
|
||||
* <p>
|
||||
* 架构说明:
|
||||
* - 状态同步:委托给 {@link OrderLifecycleManager}
|
||||
* - 派单决策:使用新的 {@link DispatchEngine}
|
||||
* - 保洁员状态:由 {@link com.viewsh.module.ops.environment.integration.listener.CleanerStateChangeListener} 处理
|
||||
* - 通知逻辑:由 {@link CleanOrderEventHandler} 处理
|
||||
* <p>
|
||||
* 变更历史:
|
||||
* - Phase 3: 使用新的调度引擎和生命周期管理器
|
||||
* - Phase 3: 移除直接的状态管理代码(改为事件驱动)
|
||||
* - Phase 3: 瘦身代码,专注保洁业务编排
|
||||
* - 直接操作主表和扩展表,不依赖 OpsOrderService
|
||||
* - 使用公用组件 OrderIdGenerator、OrderCodeGenerator
|
||||
* - 使用事件发布器 OrderEventPublisher
|
||||
* - 状态同步委托给 OrderLifecycleManager
|
||||
* - 派单决策使用 DispatchEngine
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@@ -54,9 +49,6 @@ import java.time.LocalDateTime;
|
||||
@Service
|
||||
public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
|
||||
@Resource
|
||||
private OpsOrderService opsOrderService;
|
||||
|
||||
@Resource
|
||||
private OpsOrderMapper opsOrderMapper;
|
||||
|
||||
@@ -64,13 +56,16 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
private OpsOrderCleanExtMapper cleanExtMapper;
|
||||
|
||||
@Resource
|
||||
private OrderQueueService orderQueueService;
|
||||
private OrderIdGenerator orderIdGenerator;
|
||||
|
||||
// @Resource
|
||||
// private CleanerAreaAssignStrategy cleanerAreaAssignStrategy;
|
||||
//
|
||||
// @Resource
|
||||
// private CleanerStatusService cleanerStatusService;
|
||||
@Resource
|
||||
private OrderCodeGenerator orderCodeGenerator;
|
||||
|
||||
@Resource
|
||||
private OrderEventPublisher orderEventPublisher;
|
||||
|
||||
@Resource
|
||||
private OrderQueueService orderQueueService;
|
||||
|
||||
@Resource
|
||||
private DispatchEngine dispatchEngine;
|
||||
@@ -79,20 +74,40 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
private OrderLifecycleManager orderLifecycleManager;
|
||||
|
||||
@Resource
|
||||
private CleanOrderEventHandler cleanOrderEventHandler;
|
||||
|
||||
@Resource
|
||||
private OrderEventPublisher orderEventPublisher;
|
||||
private CleanOrderEventListener cleanOrderEventListener;
|
||||
|
||||
// ==================== 工单创建 ====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createAutoCleanOrder(CleanOrderAutoCreateReqDTO createReq) {
|
||||
// 1. 调用基础服务创建工单
|
||||
Long orderId = opsOrderService.createOrder(createReq);
|
||||
// 1. 生成ID和编号
|
||||
Long orderId = orderIdGenerator.generate();
|
||||
String orderCode = orderCodeGenerator.generate("CLEAN");
|
||||
|
||||
// 2. 创建保洁扩展信息
|
||||
// 2. 构建主表数据
|
||||
OpsOrderDO order = OpsOrderDO.builder()
|
||||
.id(orderId)
|
||||
.orderCode(orderCode)
|
||||
.orderType("CLEAN")
|
||||
.title(createReq.getTitle())
|
||||
.description(createReq.getDescription())
|
||||
.priority(createReq.getPriority() != null ? createReq.getPriority() : PriorityEnum.P2.getPriority())
|
||||
.status(WorkOrderStatusEnum.PENDING.getStatus())
|
||||
.areaId(createReq.getAreaId())
|
||||
.location(createReq.getLocation())
|
||||
.sourceType(createReq.getSourceType() != null ? createReq.getSourceType() : "TRAFFIC")
|
||||
// IoT集成字段
|
||||
.triggerSource(createReq.getTriggerSource())
|
||||
.triggerRuleId(createReq.getTriggerRuleId())
|
||||
.triggerDeviceId(createReq.getTriggerDeviceId())
|
||||
.triggerDeviceKey(createReq.getTriggerDeviceKey())
|
||||
.build();
|
||||
|
||||
// 3. 插入主表
|
||||
opsOrderMapper.insert(order);
|
||||
|
||||
// 4. 构建扩展表数据(同一事务)
|
||||
OpsOrderCleanExtDO cleanExt = OpsOrderCleanExtDO.builder()
|
||||
.opsOrderId(orderId)
|
||||
.isAuto(1)
|
||||
@@ -102,62 +117,29 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
.build();
|
||||
cleanExtMapper.insert(cleanExt);
|
||||
|
||||
log.info("创建自动保洁工单成功: orderId={}, expectedDuration={}分钟",
|
||||
orderId, createReq.getExpectedDuration());
|
||||
log.info("创建自动保洁工单成功: orderId={}, orderCode={}, expectedDuration={}分钟, triggerSource={}",
|
||||
orderId, orderCode, createReq.getExpectedDuration(), createReq.getTriggerSource());
|
||||
|
||||
// 3. 发布工单创建事件,由 OrderCreatedEventListener 触发调度
|
||||
// 5. 发布工单创建事件,由 CleanOrderEventListener 触发调度
|
||||
OrderCreatedEvent event = OrderCreatedEvent.builder()
|
||||
.orderId(orderId)
|
||||
.orderType("CLEAN")
|
||||
.orderCode(orderCode)
|
||||
.title(createReq.getTitle())
|
||||
.areaId(createReq.getAreaId())
|
||||
.priority(PriorityEnum.fromPriority(createReq.getPriority()).getPriority())
|
||||
.createTime(java.time.LocalDateTime.now())
|
||||
.priority(order.getPriority())
|
||||
.createTime(LocalDateTime.now())
|
||||
.build()
|
||||
.addPayload("isAuto", true)
|
||||
.addPayload("expectedDuration", createReq.getExpectedDuration());
|
||||
.addPayload("expectedDuration", createReq.getExpectedDuration())
|
||||
.addPayload("triggerSource", createReq.getTriggerSource());
|
||||
|
||||
orderEventPublisher.publishOrderCreated(event);
|
||||
|
||||
return orderId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动分配工单(使用新的调度引擎)
|
||||
*/
|
||||
private void autoAssignOrder(Long orderId, Long areaId, PriorityEnum priority) {
|
||||
try {
|
||||
// 查询工单信息
|
||||
OpsOrderDO order = opsOrderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
log.warn("工单不存在,无法自动分配: orderId={}", orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建调度上下文
|
||||
OrderDispatchContext context = OrderDispatchContext.builder()
|
||||
.orderId(order.getId())
|
||||
.orderCode(order.getOrderCode())
|
||||
.orderTitle(order.getTitle())
|
||||
.businessType(order.getOrderType())
|
||||
.areaId(areaId)
|
||||
.priority(priority)
|
||||
.build();
|
||||
|
||||
// 使用调度引擎执行调度
|
||||
DispatchResult result = dispatchEngine.dispatch(context);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
log.info("自动分配成功: orderId={}, assigneeId={}, path={}",
|
||||
orderId, result.getAssigneeId(), result.getPath());
|
||||
} else {
|
||||
log.warn("自动分配失败: orderId={}, reason={}", orderId, result.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("自动分配工单失败: orderId={}", orderId, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 队列推送(兼容旧接口)====================
|
||||
// ==================== 队列推送 ====================
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -210,7 +192,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
}
|
||||
|
||||
// 语音播报
|
||||
cleanOrderEventHandler.sendNewOrderNotification(cleanerId, orderId);
|
||||
cleanOrderEventListener.sendNewOrderNotification(cleanerId, orderId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -272,7 +254,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
DispatchResult result = dispatchEngine.urgentInterrupt(orderId, queueDTO.getUserId());
|
||||
|
||||
// 6. 发送优先级升级通知
|
||||
cleanOrderEventHandler.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode());
|
||||
cleanOrderEventListener.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode());
|
||||
|
||||
return result.isSuccess();
|
||||
}
|
||||
@@ -298,7 +280,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
.build();
|
||||
|
||||
orderLifecycleManager.transition(request);
|
||||
// 注意:保洁员状态更新由 CleanerStateChangeListener 处理
|
||||
// 注意:保洁员状态更新由 CleanOrderEventListener 处理
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -362,7 +344,7 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
log.info("已自动推送下一个任务: cleanerId={}", cleanerId);
|
||||
} else {
|
||||
log.info("无等待任务,保洁员变空闲: cleanerId={}", cleanerId);
|
||||
// 状态更新由 CleanerStateChangeListener 处理
|
||||
// 状态更新由 CleanOrderEventListener 处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,17 +352,17 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
|
||||
@Override
|
||||
public void playVoiceForNewOrder(Long cleanerId) {
|
||||
cleanOrderEventHandler.sendNewOrderNotification(cleanerId, null);
|
||||
cleanOrderEventListener.sendNewOrderNotification(cleanerId, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playVoiceForQueuedOrder(Long cleanerId, int queueCount) {
|
||||
cleanOrderEventHandler.sendQueuedOrderNotification(cleanerId, queueCount);
|
||||
cleanOrderEventListener.sendQueuedOrderNotification(cleanerId, queueCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playVoiceForNextTask(Long cleanerId, int queueCount, String nextTaskTitle) {
|
||||
cleanOrderEventHandler.sendNextTaskNotification(cleanerId, queueCount, nextTaskTitle);
|
||||
cleanOrderEventListener.sendNextTaskNotification(cleanerId, queueCount, nextTaskTitle);
|
||||
}
|
||||
|
||||
// ==================== 作业时长计算 ====================
|
||||
@@ -403,7 +385,6 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
|
||||
/**
|
||||
* 记录到岗时间
|
||||
* 注意:此方法在被 @Transactional 的公共方法调用时,会参与同一事务
|
||||
*/
|
||||
private void recordArrivedTime(Long orderId) {
|
||||
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);
|
||||
@@ -418,7 +399,6 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
|
||||
/**
|
||||
* 记录完成时间
|
||||
* 注意:此方法在被 @Transactional 的公共方法调用时,会参与同一事务
|
||||
*/
|
||||
private void recordCompletedTime(Long orderId) {
|
||||
OpsOrderCleanExtDO cleanExt = cleanExtMapper.selectByOpsOrderId(orderId);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.viewsh.module.ops.environment.service.dispatch;
|
||||
|
||||
import com.viewsh.module.ops.api.queue.OrderQueueDTO;
|
||||
import com.viewsh.module.ops.api.queue.OrderQueueService;
|
||||
import com.viewsh.module.ops.core.dispatch.DispatchEngine;
|
||||
import com.viewsh.module.ops.core.dispatch.model.AssigneeStatus;
|
||||
import com.viewsh.module.ops.core.dispatch.model.DispatchDecision;
|
||||
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
|
||||
@@ -46,7 +47,7 @@ public class CleanerPriorityScheduleStrategy implements ScheduleStrategy {
|
||||
private OrderQueueService orderQueueService;
|
||||
|
||||
@Resource
|
||||
private com.viewsh.module.ops.core.dispatch.DispatchEngine dispatchEngine;
|
||||
private DispatchEngine dispatchEngine;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 工单出队并派单:QUEUED → DISPATCHED
|
||||
* 工单出队并派单:PENDING/QUEUED → DISPATCHED
|
||||
* <p>
|
||||
* 状态转换:
|
||||
* - 工单状态:QUEUED → DISPATCHED
|
||||
* - 工单状态:PENDING/QUEUED → DISPATCHED
|
||||
* - 队列状态:WAITING → PROCESSING
|
||||
*
|
||||
* @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: # 多租户相关配置项
|
||||
enable: true
|
||||
|
||||
--- #################### 工单相关配置 ####################
|
||||
|
||||
ops:
|
||||
order:
|
||||
datacenter-id: 1 # 数据中心ID(0-31)
|
||||
machine-id: 1 # 机器ID(0-31)
|
||||
|
||||
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