refactor(ops): 轨迹区域展示改用 fullAreaName 替代 buildingName/floorNo
TrajectoryRespDTO 移除 buildingName、floorNo 字段,新增 fullAreaName (完整路径如"A园区/A栋/3层/男卫")。AreaPathBuilder 新增 buildPaths 批量方法,一次查询所有父级区域避免 N+1;正则预编译为静态常量。 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,10 +7,8 @@ import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -25,6 +23,8 @@ import java.util.stream.Collectors;
|
||||
@Component
|
||||
public class AreaPathBuilder {
|
||||
|
||||
private static final Pattern DIGITS = Pattern.compile("\\d+");
|
||||
|
||||
@Resource
|
||||
private OpsBusAreaMapper opsBusAreaMapper;
|
||||
|
||||
@@ -47,7 +47,7 @@ public class AreaPathBuilder {
|
||||
// 解析父级ID列表
|
||||
List<Long> parentIds = Arrays.stream(parentPath.split("/"))
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.filter(pid -> pid.matches("\\d+"))
|
||||
.filter(pid -> DIGITS.matcher(pid).matches())
|
||||
.map(Long::parseLong)
|
||||
.filter(pid -> !pid.equals(area.getId()))
|
||||
.collect(Collectors.toList());
|
||||
@@ -105,4 +105,87 @@ public class AreaPathBuilder {
|
||||
return buildPath(area);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量构建区域路径(只执行一次父级查询)
|
||||
*
|
||||
* @param areas 区域对象集合
|
||||
* @return areaId → 完整路径 的映射
|
||||
*/
|
||||
public Map<Long, String> buildPaths(Collection<OpsBusAreaDO> areas) {
|
||||
if (areas == null || areas.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 1. 收集所有父级 ID
|
||||
Set<Long> allParentIds = new HashSet<>();
|
||||
for (OpsBusAreaDO area : areas) {
|
||||
if (StrUtil.isNotEmpty(area.getParentPath())) {
|
||||
for (String idStr : area.getParentPath().split("/")) {
|
||||
if (StrUtil.isNotBlank(idStr) && DIGITS.matcher(idStr).matches()) {
|
||||
allParentIds.add(Long.parseLong(idStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 排除自身已在集合中的
|
||||
Set<Long> areaIds = areas.stream().map(OpsBusAreaDO::getId).collect(Collectors.toSet());
|
||||
allParentIds.removeAll(areaIds);
|
||||
|
||||
// 2. 一次性查询所有父级
|
||||
Map<Long, String> parentNameMap = new HashMap<>();
|
||||
for (OpsBusAreaDO area : areas) {
|
||||
parentNameMap.put(area.getId(), area.getAreaName());
|
||||
}
|
||||
if (!allParentIds.isEmpty()) {
|
||||
List<OpsBusAreaDO> parents = opsBusAreaMapper.selectBatchIds(allParentIds);
|
||||
if (parents != null) {
|
||||
for (OpsBusAreaDO parent : parents) {
|
||||
parentNameMap.put(parent.getId(), parent.getAreaName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 纯内存拼路径
|
||||
Map<Long, String> result = new HashMap<>(areas.size());
|
||||
for (OpsBusAreaDO area : areas) {
|
||||
result.put(area.getId(), buildPathFromCache(area, parentNameMap));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用预加载的名称缓存构建路径(纯内存,无 DB 查询)
|
||||
*/
|
||||
private String buildPathFromCache(OpsBusAreaDO area, Map<Long, String> nameMap) {
|
||||
String parentPath = area.getParentPath();
|
||||
if (StrUtil.isEmpty(parentPath)) {
|
||||
return area.getAreaName();
|
||||
}
|
||||
|
||||
List<Long> parentIds = Arrays.stream(parentPath.split("/"))
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.filter(pid -> DIGITS.matcher(pid).matches())
|
||||
.map(Long::parseLong)
|
||||
.filter(pid -> !pid.equals(area.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 去重相邻重复
|
||||
List<String> segments = new ArrayList<>();
|
||||
Long lastId = null;
|
||||
for (Long pid : parentIds) {
|
||||
if (!pid.equals(lastId)) {
|
||||
String name = nameMap.get(pid);
|
||||
if (name != null) {
|
||||
segments.add(name);
|
||||
}
|
||||
lastId = pid;
|
||||
}
|
||||
}
|
||||
|
||||
if (segments.isEmpty()) {
|
||||
return area.getAreaName();
|
||||
}
|
||||
return String.join("/", segments) + "/" + area.getAreaName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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 AreaStayStatsDTO {
|
||||
|
||||
@Schema(description = "区域名称", example = "男卫")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "完整区域路径", example = "A园区/A栋/3层/男卫")
|
||||
private String fullAreaName;
|
||||
|
||||
@Schema(description = "总停留时长(秒)", example = "3600")
|
||||
private Long totalStaySeconds;
|
||||
|
||||
@Schema(description = "访问次数", example = "12")
|
||||
private Long visitCount;
|
||||
|
||||
}
|
||||
@@ -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 = "管理后台 - 时段出入趋势 Response DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HourlyTrendDTO {
|
||||
|
||||
@Schema(description = "小时(0-23)", example = "8")
|
||||
private Integer hour;
|
||||
|
||||
@Schema(description = "进入次数", example = "8")
|
||||
private Long enterCount;
|
||||
|
||||
@Schema(description = "离开次数", example = "6")
|
||||
private Long leaveCount;
|
||||
|
||||
}
|
||||
@@ -32,11 +32,8 @@ public class TrajectoryRespDTO {
|
||||
@Schema(description = "区域名称", example = "A座2楼男卫")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "楼栋名称", example = "A栋")
|
||||
private String buildingName;
|
||||
|
||||
@Schema(description = "楼层号", example = "2")
|
||||
private Integer floorNo;
|
||||
@Schema(description = "完整区域路径", example = "A园区/A栋/3层/男卫")
|
||||
private String fullAreaName;
|
||||
|
||||
@Schema(description = "Beacon MAC", example = "F0:C8:60:1D:10:BB")
|
||||
private String beaconMac;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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
|
||||
* <p>
|
||||
* 用于 summary / hourly-trend / area-stay-stats 三个统计接口
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 轨迹统计查询 Request DTO")
|
||||
@Data
|
||||
public class TrajectoryStatsReqDTO {
|
||||
|
||||
@Schema(description = "查询日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-04-05")
|
||||
@NotNull(message = "查询日期不能为空")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate date;
|
||||
|
||||
@Schema(description = "设备ID(不传=全部设备汇总)", example = "31")
|
||||
private Long deviceId;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user