feat(ops): 工牌实时状态增加物理位置、电量和工单信息

BadgeRealtimeStatusRespDTO 新增物理位置(IoT 轨迹检测 RPC)、
电量(IoT 设备属性 RPC)、当前工单信息三个维度。
RPC 调用改为串行执行避免占用 ForkJoinPool 公共线程。
设备状态写入 Redis 时同步写入区域名称。

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-04-05 15:26:43 +08:00
parent 9ffaac5c91
commit 54f78f8066
4 changed files with 151 additions and 18 deletions

View File

@@ -1,7 +1,9 @@
package com.viewsh.module.ops.environment.service.badge;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
@@ -48,6 +50,9 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Resource
private ApplicationEventPublisher eventPublisher;
@@ -400,6 +405,15 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
}
if (areaId != null) {
stringRedisTemplate.opsForHash().put(key, "currentAreaId", String.valueOf(areaId));
// 同步写入区域名称
try {
OpsBusAreaDO area = opsBusAreaMapper.selectById(areaId);
if (area != null && area.getAreaName() != null) {
stringRedisTemplate.opsForHash().put(key, "currentAreaName", area.getAreaName());
}
} catch (Exception e) {
log.warn("查询区域名称失败: areaId={}", areaId, e);
}
}
if (beaconMac != null) {
stringRedisTemplate.opsForHash().put(key, "beaconMac", beaconMac);

View File

@@ -1,10 +1,16 @@
package com.viewsh.module.ops.environment.service.badge;
import com.viewsh.framework.common.pojo.CommonResult;
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
import com.viewsh.module.iot.api.device.IotDevicePropertyQueryApi;
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO;
import com.viewsh.module.iot.api.trajectory.DeviceLocationDTO;
import com.viewsh.module.iot.api.trajectory.TrajectoryStateApi;
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.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.environment.service.badge.dto.BadgeNotifyReqDTO;
import com.viewsh.module.ops.service.area.AreaDeviceService;
import jakarta.annotation.Resource;
@@ -39,8 +45,22 @@ public class CleanBadgeServiceImpl implements CleanBadgeService {
@Resource
private IotDeviceControlApi iotDeviceControlApi;
@Resource
private TrajectoryStateApi trajectoryStateApi;
@Resource
private IotDevicePropertyQueryApi iotDevicePropertyQueryApi;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
private static final String NOTIFY_IDENTIFIER = "NOTIFY";
/**
* IoT 设备属性标识符:电池电量
*/
private static final String BATTERY_LEVEL_IDENTIFIER = "batteryLevel";
@Override
public List<BadgeStatusRespDTO> getBadgeStatusList(Long areaId, String status) {
try {
@@ -83,28 +103,52 @@ public class CleanBadgeServiceImpl implements CleanBadgeService {
}
}
/**
* 获取工牌实时状态详情
* <p>
* 聚合三个数据源:
* 1. 工牌设备状态Redis本地
* 2. 工牌物理位置IoT TrajectoryStateApiRPC
* 3. 设备电量IoT IotDevicePropertyQueryApiRPC
*/
@Override
public BadgeRealtimeStatusRespDTO getBadgeRealtimeStatus(Long badgeId) {
try {
// 1. 获取工牌状态
// 1. 获取工牌设备状态Redis
BadgeDeviceStatusDTO status = badgeDeviceStatusService.getBadgeStatus(badgeId);
if (status == null) {
log.warn("[getBadgeRealtimeStatus] 工牌状态不存在: badgeId={}", badgeId);
return null;
}
// 2. 构建响应
return BadgeRealtimeStatusRespDTO.builder()
// 2. 查询工牌物理位置 + 电量
DeviceLocationDTO location = queryPhysicalLocation(badgeId);
Integer batteryLevel = queryBatteryLevel(badgeId);
// 3. 组装响应
BadgeRealtimeStatusRespDTO.BadgeRealtimeStatusRespDTOBuilder builder = 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();
.batteryLevel(batteryLevel)
.onlineTime(formatTimestamp(status.getLastHeartbeatTime()));
// 物理位置
if (location != null && Boolean.TRUE.equals(location.getInArea())) {
builder.isInArea(true)
.areaId(location.getAreaId())
.areaName(queryAreaNameById(location.getAreaId()));
} else {
builder.isInArea(false);
}
// 当前工单信息
builder.currentOrderId(status.getCurrentOpsOrderId())
.currentOrderStatus(status.getCurrentOrderStatus())
.orderAreaId(status.getCurrentAreaId())
.orderAreaName(status.getCurrentAreaName());
return builder.build();
} catch (Exception e) {
log.error("[getBadgeRealtimeStatus] 查询工牌实时状态失败: badgeId={}", badgeId, e);
@@ -112,6 +156,61 @@ public class CleanBadgeServiceImpl implements CleanBadgeService {
}
}
/**
* 查询工牌物理位置(来自 IoT 轨迹检测 RPC
*
* @return 位置信息,查询失败返回 null
*/
private DeviceLocationDTO queryPhysicalLocation(Long badgeId) {
try {
CommonResult<DeviceLocationDTO> result = trajectoryStateApi.getCurrentLocation(badgeId);
if (result != null && result.isSuccess()) {
return result.getData();
}
return null;
} catch (Exception e) {
log.warn("[getBadgeRealtimeStatus] 查询工牌物理位置失败,降级为不在区域: badgeId={}", badgeId, e);
return null;
}
}
/**
* 查询电量(来自 IoT 设备属性 RPC
*
* @return 电量百分比0-100查询失败返回 null
*/
private Integer queryBatteryLevel(Long badgeId) {
try {
CommonResult<Map<String, Object>> result = iotDevicePropertyQueryApi.getLatestProperties(badgeId);
if (result != null && result.isSuccess() && result.getData() != null) {
Object batteryObj = result.getData().get(BATTERY_LEVEL_IDENTIFIER);
if (batteryObj instanceof Number) {
return ((Number) batteryObj).intValue();
}
}
return null;
} catch (Exception e) {
log.warn("[getBadgeRealtimeStatus] 查询工牌电量失败: badgeId={}", badgeId, e);
return null;
}
}
/**
* 根据区域ID查询区域名称
*/
private String queryAreaNameById(Long areaId) {
if (areaId == null) {
return null;
}
try {
OpsBusAreaDO area = opsBusAreaMapper.selectById(areaId);
return area != null ? area.queryAreaNameById() : null;
} catch (Exception e) {
log.warn("[getBadgeRealtimeStatus] 查询区域名称失败: areaId={}", areaId, e);
return null;
}
}
@Override
public void sendBadgeNotify(BadgeNotifyReqDTO req) {
try {

View File

@@ -9,7 +9,10 @@ import lombok.NoArgsConstructor;
/**
* 工牌实时状态详情响应 DTO
* <p>
* 用于工牌详情页展示
* 用于工牌详情页展示,包含:
* - 设备基础信息(状态、电量、上线时间)
* - 工牌物理位置(来自 IoT 轨迹检测)
* - 当前工单信息(来自工牌状态 Redis
*
* @author lzh
*/
@@ -20,6 +23,8 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
public class BadgeRealtimeStatusRespDTO {
// ==================== 设备基础信息 ====================
@Schema(description = "设备ID", example = "3001")
private Long deviceId;
@@ -32,18 +37,31 @@ public class BadgeRealtimeStatusRespDTO {
@Schema(description = "电量0-100", example = "72")
private Integer batteryLevel;
@Schema(description = "最后心跳时间", example = "2026-01-23 15:00:30")
private String lastHeartbeatTime;
@Schema(description = "设备上线时间", example = "2026-01-23 15:00:30")
private String onlineTime;
@Schema(description = "信号强度dBm", example = "-42")
private Integer rssi;
// ==================== 工牌物理位置(轨迹检测) ====================
@Schema(description = "是否在区域内", example = "true")
@Schema(description = "是否在区域内(基于 IoT 信标检测)", example = "true")
private Boolean isInArea;
@Schema(description = "当前区域ID", example = "101")
@Schema(description = "当前物理所在区域ID", example = "101")
private Long areaId;
@Schema(description = "当前区域名称", example = "A区洗手间")
@Schema(description = "当前物理所在区域名称", example = "A区洗手间")
private String areaName;
// ==================== 当前工单信息 ====================
@Schema(description = "当前工单ID", example = "1234567890")
private Long currentOrderId;
@Schema(description = "当前工单状态", example = "ARRIVED")
private String currentOrderStatus;
@Schema(description = "工单目标区域ID", example = "101")
private Long orderAreaId;
@Schema(description = "工单目标区域名称", example = "A区洗手间")
private String orderAreaName;
}

View File

@@ -2,6 +2,7 @@ package com.viewsh.module.ops.framework.rpc.config;
import com.viewsh.module.infra.api.file.FileApi;
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
import com.viewsh.module.iot.api.device.IotDevicePropertyQueryApi;
import com.viewsh.module.iot.api.device.IotDeviceQueryApi;
import com.viewsh.module.iot.api.device.IotDeviceStatusQueryApi;
import com.viewsh.module.iot.api.trajectory.TrajectoryStateApi;
@@ -17,6 +18,7 @@ import org.springframework.context.annotation.Configuration;
AdminUserApi.class,
SocialUserApi.class,
IotDeviceControlApi.class,
IotDevicePropertyQueryApi.class,
IotDeviceQueryApi.class,
IotDeviceStatusQueryApi.class,
TrajectoryStateApi.class,