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:
lzh
2026-02-02 22:42:45 +08:00
parent bdf5b640b0
commit 955c825e2c
43 changed files with 3312 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

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

View File

@@ -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();
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}
}

View File

@@ -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));
}
// ==========================================