feat(trajectory): 新增轨迹后台查询与实时位置接口
- 新增轨迹分页、时间线、统计摘要等查询 DTO\n- 提供轨迹后台控制器,支持工牌下拉、轨迹查询、实时位置查询\n- 接入 TrajectoryStateApi 的 Feign 配置,打通 Ops 对 IoT 实时位置状态的读取
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 工牌设备精简 Response DTO(下拉列表用)
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 工牌设备精简信息(下拉列表用)")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BadgeSimpleRespDTO {
|
||||
|
||||
@Schema(description = "设备ID", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备Key", example = "09207455611")
|
||||
private String deviceKey;
|
||||
|
||||
@Schema(description = "设备备注名称", example = "1号工牌")
|
||||
private String nickname;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 设备实时位置 Response DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 设备实时位置 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeviceCurrentLocationDTO {
|
||||
|
||||
@Schema(description = "设备ID", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "当前所在区域ID", example = "1301")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "区域名称", example = "A座2楼男卫")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "进入时间(毫秒时间戳)", example = "1711872600000")
|
||||
private Long enterTime;
|
||||
|
||||
@Schema(description = "匹配的Beacon MAC", example = "F0:C8:60:1D:10:BB")
|
||||
private String beaconMac;
|
||||
|
||||
@Schema(description = "是否在某区域内")
|
||||
private Boolean inArea;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.viewsh.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* 轨迹分页查询 Request DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 设备轨迹分页查询 Request DTO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class TrajectoryPageReqDTO extends PageParam {
|
||||
|
||||
@Schema(description = "设备ID", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "区域ID", example = "1301")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "进入时间范围(开始时间, 结束时间)")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
@Size(min = 2, max = 2, message = "进入时间范围必须包含开始时间和结束时间")
|
||||
private LocalDateTime[] enterTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 轨迹记录 Response DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 设备轨迹记录 Response DTO")
|
||||
@Data
|
||||
public class TrajectoryRespDTO {
|
||||
|
||||
@Schema(description = "记录ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "设备ID", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备名称", example = "badge_001")
|
||||
private String deviceName;
|
||||
|
||||
@Schema(description = "设备备注名称", example = "1号工牌")
|
||||
private String nickname;
|
||||
|
||||
@Schema(description = "区域ID", example = "1301")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "区域名称", example = "A座2楼男卫")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "楼栋名称", example = "A栋")
|
||||
private String buildingName;
|
||||
|
||||
@Schema(description = "楼层号", example = "2")
|
||||
private Integer floorNo;
|
||||
|
||||
@Schema(description = "Beacon MAC", example = "F0:C8:60:1D:10:BB")
|
||||
private String beaconMac;
|
||||
|
||||
@Schema(description = "进入时间")
|
||||
private LocalDateTime enterTime;
|
||||
|
||||
@Schema(description = "离开时间")
|
||||
private LocalDateTime leaveTime;
|
||||
|
||||
@Schema(description = "停留时长(秒)", example = "300")
|
||||
private Integer durationSeconds;
|
||||
|
||||
@Schema(description = "离开原因", example = "SIGNAL_LOSS")
|
||||
private String leaveReason;
|
||||
|
||||
@Schema(description = "进入时RSSI", example = "-65")
|
||||
private Integer enterRssi;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 轨迹统计摘要 Response DTO
|
||||
* <p>
|
||||
* 用于 KPI 卡片展示
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 轨迹统计摘要 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TrajectorySummaryDTO {
|
||||
|
||||
@Schema(description = "总轨迹记录数(含未关闭)", example = "42")
|
||||
private Long totalRecords;
|
||||
|
||||
@Schema(description = "已完成记录数(有离开时间)", example = "38")
|
||||
private Long completedRecords;
|
||||
|
||||
@Schema(description = "覆盖区域数", example = "8")
|
||||
private Long coveredAreaCount;
|
||||
|
||||
@Schema(description = "总停留时长(秒)", example = "28800")
|
||||
private Long totalDurationSeconds;
|
||||
|
||||
@Schema(description = "平均停留时长(秒)", example = "685")
|
||||
private Long avgDurationSeconds;
|
||||
|
||||
@Schema(description = "最长单次停留(秒)", example = "3600")
|
||||
private Long maxDurationSeconds;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.viewsh.module.ops.service.trajectory.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 轨迹时间线查询 Request DTO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 设备轨迹时间线查询 Request DTO")
|
||||
@Data
|
||||
public class TrajectoryTimelineReqDTO {
|
||||
|
||||
@Schema(description = "设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "31")
|
||||
@NotNull(message = "设备ID不能为空")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "查询日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-03-30")
|
||||
@NotNull(message = "查询日期不能为空")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate date;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.viewsh.module.ops.controller.admin.trajectory;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceQueryApi;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceSimpleRespDTO;
|
||||
import com.viewsh.module.iot.api.trajectory.DeviceLocationDTO;
|
||||
import com.viewsh.module.iot.api.trajectory.TrajectoryStateApi;
|
||||
import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO;
|
||||
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaRespVO;
|
||||
import com.viewsh.module.ops.environment.service.trajectory.DeviceTrajectoryService;
|
||||
import com.viewsh.module.ops.service.area.AreaDeviceService;
|
||||
import com.viewsh.module.ops.service.area.OpsBusAreaService;
|
||||
import com.viewsh.module.ops.service.trajectory.dto.*;
|
||||
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 jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 设备轨迹
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Tag(name = "管理后台 - 设备轨迹")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ops/trajectory")
|
||||
@Validated
|
||||
public class TrajectoryController {
|
||||
|
||||
@Resource
|
||||
private DeviceTrajectoryService trajectoryService;
|
||||
|
||||
@Resource
|
||||
private TrajectoryStateApi trajectoryStateApi;
|
||||
|
||||
@Resource
|
||||
private AreaDeviceService areaDeviceService;
|
||||
|
||||
@Resource
|
||||
private OpsBusAreaService opsBusAreaService;
|
||||
|
||||
@Resource
|
||||
private IotDeviceQueryApi iotDeviceQueryApi;
|
||||
|
||||
// ==================== 工牌设备下拉列表 ====================
|
||||
|
||||
@GetMapping("/badge-list")
|
||||
@Operation(summary = "获取工牌设备下拉列表(轨迹页面用)")
|
||||
@PreAuthorize("@ss.hasPermission('ops:trajectory:query')")
|
||||
public CommonResult<List<BadgeSimpleRespDTO>> getBadgeSimpleList() {
|
||||
// 查询所有 relationType=BADGE 的设备关联
|
||||
List<OpsAreaDeviceRelationDO> relations = areaDeviceService.listAllByType("BADGE");
|
||||
|
||||
// 按 deviceId 去重(同一设备可能绑定多个区域)
|
||||
Map<Long, OpsAreaDeviceRelationDO> uniqueDevices = relations.stream()
|
||||
.collect(Collectors.toMap(
|
||||
OpsAreaDeviceRelationDO::getDeviceId,
|
||||
Function.identity(),
|
||||
(a, b) -> a,
|
||||
LinkedHashMap::new));
|
||||
|
||||
// 批量查询设备信息以获取 nickname
|
||||
Set<Long> deviceIds = uniqueDevices.keySet();
|
||||
Map<Long, IotDeviceSimpleRespDTO> deviceMap = Collections.emptyMap();
|
||||
if (!deviceIds.isEmpty()) {
|
||||
try {
|
||||
CommonResult<List<IotDeviceSimpleRespDTO>> deviceResult =
|
||||
iotDeviceQueryApi.batchGetDevices(deviceIds);
|
||||
if (deviceResult != null && deviceResult.getData() != null) {
|
||||
deviceMap = deviceResult.getData().stream()
|
||||
.collect(Collectors.toMap(IotDeviceSimpleRespDTO::getId, Function.identity()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[badge-list] 批量查询设备信息失败,降级返回无 nickname", e);
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, IotDeviceSimpleRespDTO> finalDeviceMap = deviceMap;
|
||||
List<BadgeSimpleRespDTO> result = uniqueDevices.values().stream()
|
||||
.map(r -> {
|
||||
BadgeSimpleRespDTO dto = BadgeSimpleRespDTO.builder()
|
||||
.deviceId(r.getDeviceId())
|
||||
.deviceKey(r.getDeviceKey())
|
||||
.build();
|
||||
IotDeviceSimpleRespDTO device = finalDeviceMap.get(r.getDeviceId());
|
||||
if (device != null) {
|
||||
dto.setNickname(device.getNickname());
|
||||
}
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return success(result);
|
||||
}
|
||||
|
||||
// ==================== 轨迹查询 ====================
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得设备轨迹分页")
|
||||
@PreAuthorize("@ss.hasPermission('ops:trajectory:query')")
|
||||
public CommonResult<PageResult<TrajectoryRespDTO>> getTrajectoryPage(
|
||||
@Valid TrajectoryPageReqDTO pageReq) {
|
||||
return success(trajectoryService.getTrajectoryPage(pageReq));
|
||||
}
|
||||
|
||||
@GetMapping("/timeline")
|
||||
@Operation(summary = "获得设备某天的轨迹时间线")
|
||||
@PreAuthorize("@ss.hasPermission('ops:trajectory:query')")
|
||||
public CommonResult<List<TrajectoryRespDTO>> getTimeline(@Valid TrajectoryTimelineReqDTO req) {
|
||||
return success(trajectoryService.getTimeline(req.getDeviceId(), req.getDate()));
|
||||
}
|
||||
|
||||
@GetMapping("/summary")
|
||||
@Operation(summary = "获得设备某天的轨迹统计摘要")
|
||||
@PreAuthorize("@ss.hasPermission('ops:trajectory:query')")
|
||||
public CommonResult<TrajectorySummaryDTO> getSummary(@Valid TrajectoryTimelineReqDTO req) {
|
||||
return success(trajectoryService.getSummary(req.getDeviceId(), req.getDate()));
|
||||
}
|
||||
|
||||
// ==================== 实时位置 ====================
|
||||
|
||||
@GetMapping("/current-location")
|
||||
@Operation(summary = "获得设备当前位置(实时)")
|
||||
@PreAuthorize("@ss.hasPermission('ops:trajectory:query')")
|
||||
public CommonResult<DeviceCurrentLocationDTO> getCurrentLocation(
|
||||
@Parameter(description = "设备ID", required = true, example = "31")
|
||||
@RequestParam("deviceId") Long deviceId) {
|
||||
// 调用 IoT RPC 查询设备实时位置
|
||||
CommonResult<DeviceLocationDTO> result;
|
||||
try {
|
||||
result = trajectoryStateApi.getCurrentLocation(deviceId);
|
||||
} catch (Exception e) {
|
||||
log.error("[current-location] IoT 服务调用失败:deviceId={}", deviceId, e);
|
||||
throw new RuntimeException("IoT 服务不可用,无法查询设备实时位置", e);
|
||||
}
|
||||
|
||||
if (result == null || !result.isSuccess() || result.getData() == null
|
||||
|| !Boolean.TRUE.equals(result.getData().getInArea())) {
|
||||
return success(DeviceCurrentLocationDTO.builder()
|
||||
.deviceId(deviceId)
|
||||
.inArea(false)
|
||||
.build());
|
||||
}
|
||||
|
||||
DeviceLocationDTO location = result.getData();
|
||||
// 通过 Service 查询区域名称(不直接注入 Mapper)
|
||||
String areaName = null;
|
||||
if (location.getAreaId() != null) {
|
||||
try {
|
||||
OpsBusAreaRespVO area = opsBusAreaService.getArea(location.getAreaId());
|
||||
areaName = area.getAreaName();
|
||||
} catch (Exception e) {
|
||||
log.warn("[current-location] 查询区域名称失败:areaId={}", location.getAreaId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return success(DeviceCurrentLocationDTO.builder()
|
||||
.deviceId(location.getDeviceId())
|
||||
.areaId(location.getAreaId())
|
||||
.areaName(areaName)
|
||||
.enterTime(location.getEnterTime())
|
||||
.beaconMac(location.getBeaconMac())
|
||||
.inArea(true)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.viewsh.module.infra.api.file.FileApi;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceQueryApi;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceStatusQueryApi;
|
||||
import com.viewsh.module.iot.api.trajectory.TrajectoryStateApi;
|
||||
import com.viewsh.module.system.api.notify.NotifyMessageSendApi;
|
||||
import com.viewsh.module.system.api.social.SocialUserApi;
|
||||
import com.viewsh.module.system.api.user.AdminUserApi;
|
||||
@@ -18,6 +19,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
IotDeviceControlApi.class,
|
||||
IotDeviceQueryApi.class,
|
||||
IotDeviceStatusQueryApi.class,
|
||||
TrajectoryStateApi.class,
|
||||
FileApi.class
|
||||
})
|
||||
public class RpcConfiguration {
|
||||
|
||||
Reference in New Issue
Block a user