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:
lzh
2026-04-05 15:25:47 +08:00
parent 9780d6c3f7
commit 368fa90156
5 changed files with 182 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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