feat(ops,iot): 保洁前端 API 层和区域管理新增
新增保洁业务前端 API 接口层(工牌、工单、仪表盘)和运营区域管理完整功能,包含 Service/Controller/Test 三层结构。 主要功能: 1. IoT 设备查询 API(RPC 接口) - IotDeviceQueryApi: 提供设备简化信息查询 - IotDeviceSimpleRespDTO: 设备简化 DTO 2. 保洁工牌管理 - CleanBadgeService/Impl: 工牌通知、优先级调整、手动完成 - BadgeNotifyReqDTO/UpgradePriorityReqDTO/ManualCompleteOrderReqDTO 3. 保洁工单管理 - CleanWorkOrderService/Impl: 工单时间线查询 4. 保洁仪表盘 - CleanDashboardService/Impl: 快速统计(待处理/进行中/已完成/在线工牌数) - QuickStatsRespDTO: 快速统计 DTO 5. 运营区域管理(Ops Biz) - OpsBusAreaService/Impl: 区域 CRUD(支持树形结构、分页查询) - AreaDeviceRelationService/Impl: 区域设备关联管理(绑定/解绑/批量更新) - OpsBusAreaMapper/AreaDeviceRelationMapper: 扩展 MyBatis 批量方法 - 7 个 VO 类:CreateReqVO/UpdateReqVO/PageReqVO/RespVO/BindReqVO/RelationRespVO/DeviceUpdateReqVO 6. 前端 Controller(Ops Server) - OpsBusAreaController: 区域管理 REST API(11 个接口) - AreaDeviceRelationController: 设备关联 REST API(8 个接口) - CleanBadgeController: 工牌管理 REST API(5 个接口) - CleanDashboardController: 仪表盘 REST API(1 个接口) - CleanDeviceController: 设备管理 REST API(2 个接口) - CleanWorkOrderController: 工单管理 REST API(2 个接口) 7. 测试覆盖 - OpsBusAreaServiceTest: 区域服务测试(284 行) - AreaDeviceRelationServiceTest: 设备关联测试(240 行) - OpsBusAreaControllerTest: 区域 Controller 测试(186 行) - AreaDeviceRelationControllerTest: 设备关联 Controller 测试(182 行) 8. API 层扩展 - ErrorCodeConstants: 错误码常量(区域、设备关联) - NotifyTypeEnum: 通知类型枚举(语音、文本、震动) - 4 个 Badge/Order DTO: BadgeStatusRespDTO/BadgeRealtimeStatusRespDTO/OrderTimelineRespDTO 9. RPC 配置 - RpcConfiguration: 注入 IotDeviceQueryApi 影响模块:Ops API、Ops Biz、Ops Server、Ops Environment Biz、IoT API、IoT Server Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package com.viewsh.module.ops.environment.dal.dataobject;
|
||||
|
||||
import com.viewsh.module.ops.enums.NotifyTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 工牌通知请求 DTO
|
||||
* <p>
|
||||
* 用于发送工牌设备通知(语音、震动)
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 工牌通知 Request DTO")
|
||||
@Data
|
||||
public class BadgeNotifyReqDTO {
|
||||
|
||||
@Schema(description = "工牌设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||
@NotNull(message = "工牌设备ID不能为空")
|
||||
private Long badgeId;
|
||||
|
||||
@Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "VOICE")
|
||||
@NotNull(message = "通知类型不能为空")
|
||||
private NotifyTypeEnum type;
|
||||
|
||||
@Schema(description = "语音内容(仅语音通知需要)", example = "请注意,您有待处理工单")
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.viewsh.module.ops.environment.dal.dataobject;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 手动完成工单请求 DTO
|
||||
* <p>
|
||||
* 管理员手动完成工单的兜底操作
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 手动完成工单 Request DTO")
|
||||
@Data
|
||||
public class ManualCompleteOrderReqDTO {
|
||||
|
||||
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||
@NotNull(message = "工单ID不能为空")
|
||||
private Long orderId;
|
||||
|
||||
@Schema(description = "备注", example = "管理员手动完成")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.viewsh.module.ops.environment.dal.dataobject;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 升级工单优先级请求 DTO
|
||||
* <p>
|
||||
* 用于将普通工单升级为 P0 紧急工单
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 升级工单优先级 Request DTO")
|
||||
@Data
|
||||
public class UpgradePriorityReqDTO {
|
||||
|
||||
@Schema(description = "工单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||
@NotNull(message = "工单ID不能为空")
|
||||
private Long orderId;
|
||||
|
||||
@Schema(description = "升级原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户投诉急需处理")
|
||||
@NotBlank(message = "升级原因不能为空")
|
||||
private String reason;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.viewsh.module.ops.environment.service.badge;
|
||||
|
||||
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeRealtimeStatusRespDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeStatusRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.BadgeNotifyReqDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保洁工牌服务接口
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 查询工牌实时状态列表
|
||||
* 2. 查询工牌实时状态详情
|
||||
* 3. 发送工牌通知(语音/震动)
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - 工牌状态来源于 Redis(通过 BadgeDeviceStatusService)
|
||||
* - 设备基本信息来源于 iot_device 表
|
||||
* - 区域关联来源于 ops_area_device_relation 表
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface CleanBadgeService {
|
||||
|
||||
/**
|
||||
* 获取工牌实时状态列表
|
||||
* <p>
|
||||
* 支持按区域和状态筛选
|
||||
*
|
||||
* @param areaId 区域ID(可选)
|
||||
* @param status 状态筛选(可选):IDLE/BUSY/OFFLINE/PAUSED
|
||||
* @return 工牌状态列表
|
||||
*/
|
||||
List<BadgeStatusRespDTO> getBadgeStatusList(Long areaId, String status);
|
||||
|
||||
/**
|
||||
* 获取工牌实时状态详情
|
||||
*
|
||||
* @param badgeId 工牌设备ID
|
||||
* @return 工牌实时状态详情
|
||||
*/
|
||||
BadgeRealtimeStatusRespDTO getBadgeRealtimeStatus(Long badgeId);
|
||||
|
||||
/**
|
||||
* 发送工牌通知
|
||||
* <p>
|
||||
* 支持语音和震动通知
|
||||
*
|
||||
* @param req 通知请求
|
||||
*/
|
||||
void sendBadgeNotify(BadgeNotifyReqDTO req);
|
||||
|
||||
/**
|
||||
* 获取工牌今日完成工单数
|
||||
*
|
||||
* @param deviceId 设备ID(通过设备ID查找关联的用户)
|
||||
* @return 今日完成工单数
|
||||
*/
|
||||
Integer getTodayCompletedCount(Long deviceId);
|
||||
|
||||
/**
|
||||
* 获取工牌今日工作时长(分钟)
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 今日工作时长(分钟)
|
||||
*/
|
||||
Integer getTodayWorkMinutes(Long deviceId);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.viewsh.module.ops.environment.service.badge;
|
||||
|
||||
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO;
|
||||
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeRealtimeStatusRespDTO;
|
||||
import com.viewsh.module.ops.api.clean.BadgeStatusRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.BadgeNotifyReqDTO;
|
||||
import com.viewsh.module.ops.service.area.AreaDeviceService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 保洁工牌服务实现
|
||||
* <p>
|
||||
* 工牌状态数据来源:Redis (ops:badge:status:{deviceId})
|
||||
* 设备基本信息来源:iot_device 表
|
||||
* 区域关联来源:ops_area_device_relation 表
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CleanBadgeServiceImpl implements CleanBadgeService {
|
||||
|
||||
@Resource
|
||||
private BadgeDeviceStatusService badgeDeviceStatusService;
|
||||
|
||||
@Resource
|
||||
private AreaDeviceService areaDeviceService;
|
||||
|
||||
@Resource
|
||||
private IotDeviceControlApi iotDeviceControlApi;
|
||||
|
||||
private static final String NOTIFY_IDENTIFIER = "NOTIFY";
|
||||
|
||||
@Override
|
||||
public List<BadgeStatusRespDTO> getBadgeStatusList(Long areaId, String status) {
|
||||
try {
|
||||
// 1. 获取设备ID列表
|
||||
List<Long> deviceIds;
|
||||
if (areaId != null) {
|
||||
deviceIds = areaDeviceService.getDeviceIdsByAreaAndType(areaId, "BADGE");
|
||||
} else {
|
||||
// 不指定区域时,从所有活跃工牌获取
|
||||
List<BadgeDeviceStatusDTO> allActiveBadges = badgeDeviceStatusService.listActiveBadges();
|
||||
deviceIds = allActiveBadges.stream()
|
||||
.map(BadgeDeviceStatusDTO::getDeviceId)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (deviceIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 批量获取工牌状态
|
||||
List<BadgeDeviceStatusDTO> statusList = badgeDeviceStatusService.batchGetBadgeStatus(deviceIds);
|
||||
|
||||
// 3. 按状态筛选(如果指定)
|
||||
List<BadgeDeviceStatusDTO> filteredList = statusList;
|
||||
if (status != null && !status.isEmpty()) {
|
||||
filteredList = statusList.stream()
|
||||
.filter(dto -> dto.getStatus() != null)
|
||||
.filter(dto -> dto.getStatus().getCode().equalsIgnoreCase(status))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 4. 转换为响应DTO
|
||||
return filteredList.stream()
|
||||
.map(this::convertToBadgeStatusRespDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[getBadgeStatusList] 查询工牌状态列表失败: areaId={}, status={}", areaId, status, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BadgeRealtimeStatusRespDTO getBadgeRealtimeStatus(Long badgeId) {
|
||||
try {
|
||||
// 1. 获取工牌状态
|
||||
BadgeDeviceStatusDTO status = badgeDeviceStatusService.getBadgeStatus(badgeId);
|
||||
if (status == null) {
|
||||
log.warn("[getBadgeRealtimeStatus] 工牌状态不存在: badgeId={}", badgeId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 构建响应
|
||||
return BadgeRealtimeStatusRespDTO.builder()
|
||||
.deviceId(status.getDeviceId())
|
||||
.deviceKey(status.getDeviceCode())
|
||||
.status(status.getStatusCode())
|
||||
.batteryLevel(status.getBatteryLevel())
|
||||
.lastHeartbeatTime(formatTimestamp(status.getLastHeartbeatTime()))
|
||||
.rssi(null) // RSSI 需要从 IoT 模块获取,暂不实现
|
||||
.isInArea(status.getCurrentAreaId() != null)
|
||||
.areaId(status.getCurrentAreaId())
|
||||
.areaName(status.getCurrentAreaName())
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[getBadgeRealtimeStatus] 查询工牌实时状态失败: badgeId={}", badgeId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendBadgeNotify(BadgeNotifyReqDTO req) {
|
||||
try {
|
||||
Long deviceId = req.getBadgeId();
|
||||
|
||||
if (req.getType() == com.viewsh.module.ops.enums.NotifyTypeEnum.VOICE) {
|
||||
// 语音通知 - 使用 TTS 服务
|
||||
if (req.getContent() == null || req.getContent().isEmpty()) {
|
||||
log.warn("[sendBadgeNotify] 语音通知内容为空: deviceId={}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO();
|
||||
reqDTO.setDeviceId(deviceId);
|
||||
reqDTO.setIdentifier("TTS");
|
||||
reqDTO.setParams(Map.of(
|
||||
"tts_text", req.getContent(),
|
||||
"tts_flag", 0x08 // 普通通知
|
||||
));
|
||||
|
||||
iotDeviceControlApi.invokeService(reqDTO);
|
||||
log.info("[sendBadgeNotify] 语音通知发送成功: deviceId={}, content={}", deviceId, req.getContent());
|
||||
|
||||
} else if (req.getType() == com.viewsh.module.ops.enums.NotifyTypeEnum.VIBRATE) {
|
||||
// 震动通知
|
||||
IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO();
|
||||
reqDTO.setDeviceId(deviceId);
|
||||
reqDTO.setIdentifier(NOTIFY_IDENTIFIER);
|
||||
reqDTO.setParams(Map.of(
|
||||
"notify_type", "VIBRATE",
|
||||
"duration", 1000 // 震动1秒
|
||||
));
|
||||
|
||||
iotDeviceControlApi.invokeService(reqDTO);
|
||||
log.info("[sendBadgeNotify] 震动通知发送成功: deviceId={}", deviceId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[sendBadgeNotify] 发送工牌通知失败: badgeId={}, type={}",
|
||||
req.getBadgeId(), req.getType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getTodayCompletedCount(Long deviceId) {
|
||||
// 暂不实现,工牌不关联用户,无法统计工牌完成的工单数
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getTodayWorkMinutes(Long deviceId) {
|
||||
// 暂不实现,工牌不关联用户,无法统计工牌工作时长
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 转换为工牌状态响应DTO
|
||||
*/
|
||||
private BadgeStatusRespDTO convertToBadgeStatusRespDTO(BadgeDeviceStatusDTO status) {
|
||||
return BadgeStatusRespDTO.builder()
|
||||
.deviceId(status.getDeviceId())
|
||||
.deviceKey(status.getDeviceCode())
|
||||
.status(status.getStatusCode())
|
||||
.batteryLevel(status.getBatteryLevel())
|
||||
.lastHeartbeatTime(formatTimestamp(status.getLastHeartbeatTime()))
|
||||
.currentAreaId(status.getCurrentAreaId())
|
||||
.currentAreaName(status.getCurrentAreaName())
|
||||
.todayCompletedCount(0)
|
||||
.todayWorkMinutes(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间戳
|
||||
*/
|
||||
private String formatTimestamp(Long timestamp) {
|
||||
if (timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.viewsh.module.ops.environment.service.cleanorder;
|
||||
|
||||
import com.viewsh.module.ops.api.clean.OrderTimelineRespDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.ManualCompleteOrderReqDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.UpgradePriorityReqDTO;
|
||||
|
||||
/**
|
||||
* 保洁工单服务接口
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 查询工单时间轴
|
||||
* 2. 手动完成工单(兜底操作)
|
||||
* 3. 升级工单优先级(P0插队)
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - 工单时间轴数据来源于 ops_order_event 表
|
||||
* - 工单操作复用现有的 OpsOrderService
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface CleanWorkOrderService {
|
||||
|
||||
/**
|
||||
* 获取工单时间轴
|
||||
* <p>
|
||||
* 从 ops_order_event 表查询该工单的所有事件记录
|
||||
*
|
||||
* @param orderId 工单ID
|
||||
* @return 工单时间轴
|
||||
*/
|
||||
OrderTimelineRespDTO getOrderTimeline(Long orderId);
|
||||
|
||||
/**
|
||||
* 手动完成工单
|
||||
* <p>
|
||||
* 管理员手动完成工单的兜底操作
|
||||
*
|
||||
* @param req 手动完成请求
|
||||
*/
|
||||
void manualCompleteOrder(ManualCompleteOrderReqDTO req);
|
||||
|
||||
/**
|
||||
* 升级工单优先级
|
||||
* <p>
|
||||
* 将普通工单升级为 P0 紧急工单
|
||||
*
|
||||
* @param req 升级优先级请求
|
||||
*/
|
||||
void upgradePriority(UpgradePriorityReqDTO req);
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.viewsh.module.ops.environment.service.cleanorder;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.api.clean.OrderTimelineRespDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderEventDO;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderEventMapper;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.ManualCompleteOrderReqDTO;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.UpgradePriorityReqDTO;
|
||||
import com.viewsh.module.ops.service.event.OpsOrderEventService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 保洁工单服务实现
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Resource
|
||||
private OpsOrderMapper opsOrderMapper;
|
||||
|
||||
@Resource
|
||||
private OpsOrderEventMapper opsOrderEventMapper;
|
||||
|
||||
@Resource
|
||||
private OpsOrderEventService opsOrderEventService;
|
||||
|
||||
/**
|
||||
* 事件类型名称映射
|
||||
*/
|
||||
private static final Map<String, String> EVENT_TYPE_NAMES = new HashMap<>();
|
||||
|
||||
static {
|
||||
EVENT_TYPE_NAMES.put("CREATE", "工单创建");
|
||||
EVENT_TYPE_NAMES.put("ASSIGN", "工单分配");
|
||||
EVENT_TYPE_NAMES.put("ACCEPT", "接单确认");
|
||||
EVENT_TYPE_NAMES.put("ARRIVE", "到岗确认");
|
||||
EVENT_TYPE_NAMES.put("PAUSE", "工单暂停");
|
||||
EVENT_TYPE_NAMES.put("RESUME", "恢复作业");
|
||||
EVENT_TYPE_NAMES.put("COMPLETE", "工单完成");
|
||||
EVENT_TYPE_NAMES.put("CANCEL", "工单取消");
|
||||
EVENT_TYPE_NAMES.put("UPGRADE_PRIORITY", "优先级升级");
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderTimelineRespDTO getOrderTimeline(Long orderId) {
|
||||
try {
|
||||
// 1. 查询工单获取当前状态
|
||||
OpsOrderDO order = opsOrderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
log.warn("[getOrderTimeline] 工单不存在: orderId={}", orderId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 查询工单事件
|
||||
List<OpsOrderEventDO> events = opsOrderEventMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsOrderEventDO>()
|
||||
.eq(OpsOrderEventDO::getOpsOrderId, orderId)
|
||||
.orderByAsc(OpsOrderEventDO::getEventTime)
|
||||
);
|
||||
|
||||
// 3. 转换为时间轴节点
|
||||
List<OrderTimelineRespDTO.TimelineItemDTO> timeline = events.stream()
|
||||
.map(this::convertToTimelineItem)
|
||||
.toList();
|
||||
|
||||
// 4. 构建响应
|
||||
return OrderTimelineRespDTO.builder()
|
||||
.orderId(orderId)
|
||||
.currentStatus(order.getStatus())
|
||||
.timeline(timeline)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[getOrderTimeline] 查询工单时间轴失败: orderId={}", orderId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void manualCompleteOrder(ManualCompleteOrderReqDTO req) {
|
||||
try {
|
||||
Long orderId = req.getOrderId();
|
||||
|
||||
// 1. 查询工单
|
||||
OpsOrderDO order = opsOrderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
log.warn("[manualCompleteOrder] 工单不存在: orderId={}", orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
String fromStatus = order.getStatus();
|
||||
|
||||
// 2. 更新工单状态为已完成
|
||||
OpsOrderDO updateDO = new OpsOrderDO();
|
||||
updateDO.setId(orderId);
|
||||
updateDO.setStatus("COMPLETED");
|
||||
updateDO.setEndTime(LocalDateTime.now());
|
||||
opsOrderMapper.updateById(updateDO);
|
||||
|
||||
// 3. 记录事件
|
||||
opsOrderEventService.recordEvent(
|
||||
orderId,
|
||||
fromStatus,
|
||||
"COMPLETED",
|
||||
"COMPLETE",
|
||||
"ADMIN",
|
||||
null, // 管理员操作,当前未传递具体ID
|
||||
req.getRemark() != null ? req.getRemark() : "管理员手动完成"
|
||||
);
|
||||
|
||||
log.info("[manualCompleteOrder] 手动完成工单成功: orderId={}, remark={}", orderId, req.getRemark());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[manualCompleteOrder] 手动完成工单失败: orderId={}", req.getOrderId(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void upgradePriority(UpgradePriorityReqDTO req) {
|
||||
try {
|
||||
Long orderId = req.getOrderId();
|
||||
|
||||
// 1. 查询工单
|
||||
OpsOrderDO order = opsOrderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
log.warn("[upgradePriority] 工单不存在: orderId={}", orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
Integer oldPriority = order.getPriority();
|
||||
|
||||
// 2. 更新工单优先级为 P0
|
||||
OpsOrderDO updateDO = new OpsOrderDO();
|
||||
updateDO.setId(orderId);
|
||||
updateDO.setPriority(0); // P0
|
||||
opsOrderMapper.updateById(updateDO);
|
||||
|
||||
// 3. 记录事件
|
||||
opsOrderEventService.recordEvent(
|
||||
orderId,
|
||||
order.getStatus(),
|
||||
order.getStatus(),
|
||||
"UPGRADE_PRIORITY",
|
||||
"ADMIN",
|
||||
null, // 管理员操作
|
||||
String.format("优先级从 P%d 升级为 P0: %s",
|
||||
oldPriority != null ? oldPriority : 1, req.getReason())
|
||||
);
|
||||
|
||||
log.info("[upgradePriority] 升级工单优先级成功: orderId={}, oldPriority={}, reason={}",
|
||||
orderId, oldPriority, req.getReason());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[upgradePriority] 升级工单优先级失败: orderId={}", req.getOrderId(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 转换为时间轴节点
|
||||
*/
|
||||
private OrderTimelineRespDTO.TimelineItemDTO convertToTimelineItem(OpsOrderEventDO event) {
|
||||
String statusName = EVENT_TYPE_NAMES.getOrDefault(
|
||||
event.getEventType(),
|
||||
event.getEventType()
|
||||
);
|
||||
|
||||
String operator = event.getOperatorName();
|
||||
if (operator == null || operator.isEmpty()) {
|
||||
if ("SYSTEM".equals(event.getOperatorType())) {
|
||||
operator = "系统";
|
||||
} else {
|
||||
operator = "操作员";
|
||||
}
|
||||
}
|
||||
|
||||
return OrderTimelineRespDTO.TimelineItemDTO.builder()
|
||||
.status(event.getToStatus())
|
||||
.statusName(statusName)
|
||||
.time(formatDateTime(event.getEventTime()))
|
||||
.operator(operator)
|
||||
.description(event.getRemark())
|
||||
.extra(buildExtraInfo(event))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
private String formatDateTime(LocalDateTime dateTime) {
|
||||
if (dateTime == null) {
|
||||
return null;
|
||||
}
|
||||
return dateTime.format(DATE_TIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建额外信息
|
||||
*/
|
||||
private Map<String, Object> buildExtraInfo(OpsOrderEventDO event) {
|
||||
Map<String, Object> extra = new HashMap<>();
|
||||
extra.put("event_type", event.getEventType());
|
||||
extra.put("operator_type", event.getOperatorType());
|
||||
if (event.getOperatorId() != null) {
|
||||
extra.put("operator_id", event.getOperatorId());
|
||||
}
|
||||
return extra;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.viewsh.module.ops.environment.service.dashboard;
|
||||
|
||||
import com.viewsh.module.ops.api.clean.QuickStatsRespDTO;
|
||||
|
||||
/**
|
||||
* 保洁仪表盘服务接口
|
||||
* <p>
|
||||
* 职责:
|
||||
* 1. 提供快速统计数据
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
public interface CleanDashboardService {
|
||||
|
||||
/**
|
||||
* 获取快速统计
|
||||
* <p>
|
||||
* 包括:待分配数量、进行中数量、今日完成数量、在线工<E7BABF><E5B7A5><EFBFBD>数量
|
||||
*
|
||||
* @return 快速统计响应
|
||||
*/
|
||||
QuickStatsRespDTO getQuickStats();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.viewsh.module.ops.environment.service.dashboard;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
|
||||
import com.viewsh.module.ops.api.clean.QuickStatsRespDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保洁仪表盘服务实现
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CleanDashboardServiceImpl implements CleanDashboardService {
|
||||
|
||||
@Resource
|
||||
private OpsOrderMapper opsOrderMapper;
|
||||
|
||||
@Resource
|
||||
private BadgeDeviceStatusService badgeDeviceStatusService;
|
||||
|
||||
@Override
|
||||
public QuickStatsRespDTO getQuickStats() {
|
||||
try {
|
||||
// 1. 统计待分配数量 (status='PENDING' AND order_type='CLEAN')
|
||||
Integer pendingCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getStatus, "PENDING")
|
||||
.eq(OpsOrderDO::getOrderType, "CLEAN")
|
||||
));
|
||||
|
||||
// 2. 统计进行中数量 (status IN ('DISPATCHED','CONFIRMED','ARRIVED') AND order_type='CLEAN')
|
||||
Integer inProgressCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.in(OpsOrderDO::getStatus, List.of("DISPATCHED", "CONFIRMED", "ARRIVED"))
|
||||
.eq(OpsOrderDO::getOrderType, "CLEAN")
|
||||
));
|
||||
|
||||
// 3. 统计今日完成数量
|
||||
LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
|
||||
Integer completedTodayCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getStatus, "COMPLETED")
|
||||
.eq(OpsOrderDO::getOrderType, "CLEAN")
|
||||
.between(OpsOrderDO::getEndTime, todayStart, todayEnd)
|
||||
));
|
||||
|
||||
// 4. 统计在线工牌数量(从 Redis)
|
||||
List<BadgeDeviceStatusDTO> activeBadges = badgeDeviceStatusService.listActiveBadges();
|
||||
Integer onlineBadgeCount = activeBadges.size();
|
||||
|
||||
return QuickStatsRespDTO.builder()
|
||||
.pendingCount(pendingCount)
|
||||
.inProgressCount(inProgressCount)
|
||||
.completedTodayCount(completedTodayCount)
|
||||
.onlineBadgeCount(onlineBadgeCount)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[getQuickStats] 获取快速统计失败", e);
|
||||
return QuickStatsRespDTO.builder()
|
||||
.pendingCount(0)
|
||||
.inProgressCount(0)
|
||||
.completedTodayCount(0)
|
||||
.onlineBadgeCount(0)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,7 +338,7 @@ public class CleanOrderEndToEndTest {
|
||||
verify(eventLogRecorder).record(any());
|
||||
|
||||
// 2. TTS sent (orderId can be null for TTS_REQUEST events)
|
||||
verify(voiceBroadcastService).broadcast(eq(5001L), contains("请回到作业区域"), isNull());
|
||||
verify(voiceBroadcastService).broadcast(eq(5001L), contains("请回到作业区域"), eq((Long) null));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
Reference in New Issue
Block a user