feat(ops): 新增客流统计后端接口(区域汇总查询+缓存优化)
- 新增 OpsTrafficController 客流统计独立 Controller(/ops/traffic/*) - 新增区域汇总接口:getAreaTrafficRealtime/getAreaTrafficTrend(多区域ID聚合) - TrafficRealtimeRespVO 新增 yesterdayHourlyTrend 和 message 字段 - DeviceTrafficRealtimeRespVO 新增 yesterdayHourlyTrend 字段 - 区域接口添加 @Cacheable 5分钟 Redis 缓存 - loadAreaNameMap 添加本地缓存(5分钟TTL)避免重复全表扫描 - areaIds 参数双层限制 200 上限防止 DoS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 单设备/区域维度的实时客流 Response VO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 单设备/区域维度实时客流 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeviceTrafficRealtimeRespVO {
|
||||
|
||||
@Schema(description = "设备ID")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备名称")
|
||||
private String deviceName;
|
||||
|
||||
@Schema(description = "区域ID")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "区域名称")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "今日进入人数")
|
||||
private Long todayIn;
|
||||
|
||||
@Schema(description = "今日离开人数")
|
||||
private Long todayOut;
|
||||
|
||||
@Schema(description = "当前在场人数")
|
||||
private Long currentOccupancy;
|
||||
|
||||
@Schema(description = "今日小时趋势")
|
||||
private TrafficRealtimeRespVO.HourlyTrend hourlyTrend;
|
||||
|
||||
@Schema(description = "昨日小时趋势")
|
||||
private TrafficRealtimeRespVO.HourlyTrend yesterdayHourlyTrend;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单设备/区域维度的客流趋势 Response VO
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 单设备/区域维度客流趋势 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeviceTrafficTrendRespVO {
|
||||
|
||||
@Schema(description = "设备ID")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "区域ID")
|
||||
private Long areaId;
|
||||
|
||||
@Schema(description = "日期列表")
|
||||
private List<String> dates;
|
||||
|
||||
@Schema(description = "每日进入人数")
|
||||
private List<Long> inData;
|
||||
|
||||
@Schema(description = "每日离开人数")
|
||||
private List<Long> outData;
|
||||
|
||||
@Schema(description = "每日净增人数")
|
||||
private List<Long> netData;
|
||||
|
||||
@Schema(description = "总进入人数")
|
||||
private Long totalIn;
|
||||
|
||||
@Schema(description = "总离开人数")
|
||||
private Long totalOut;
|
||||
|
||||
}
|
||||
@@ -27,9 +27,15 @@ public class TrafficRealtimeRespVO {
|
||||
@Schema(description = "区域客流明细")
|
||||
private List<AreaTrafficItem> areas;
|
||||
|
||||
@Schema(description = "小时趋势")
|
||||
@Schema(description = "今日小时趋势")
|
||||
private HourlyTrend hourlyTrend;
|
||||
|
||||
@Schema(description = "昨日小时趋势")
|
||||
private HourlyTrend yesterdayHourlyTrend;
|
||||
|
||||
@Schema(description = "提示信息,如区域暂未配置客流设备")
|
||||
private String message;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.viewsh.module.ops.service.statistics;
|
||||
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DashboardStatsRespVO;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DeviceTrafficRealtimeRespVO;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DeviceTrafficTrendRespVO;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficRealtimeRespVO;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficTrendRespVO;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.WorkspaceStatsRespVO;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工单统计服务接口
|
||||
@@ -45,4 +48,58 @@ public interface OpsStatisticsService {
|
||||
*/
|
||||
TrafficTrendRespVO getTrafficTrend();
|
||||
|
||||
/**
|
||||
* 按设备查实时客流
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 单设备实时客流数据
|
||||
*/
|
||||
DeviceTrafficRealtimeRespVO getDeviceTrafficRealtime(Long deviceId);
|
||||
|
||||
/**
|
||||
* 按区域查实时客流(返回该区域下每个设备的明细)
|
||||
*
|
||||
* @param areaId 区域ID
|
||||
* @return 区域下各设备的实时客流数据
|
||||
*/
|
||||
List<DeviceTrafficRealtimeRespVO> getAreaDeviceTrafficRealtime(Long areaId);
|
||||
|
||||
/**
|
||||
* 按设备查趋势
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return 单设备客流趋势数据
|
||||
*/
|
||||
DeviceTrafficTrendRespVO getDeviceTrafficTrend(Long deviceId, LocalDate startDate, LocalDate endDate);
|
||||
|
||||
/**
|
||||
* 按区域查趋势(汇总)
|
||||
*
|
||||
* @param areaId 区域ID
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return 区域客流趋势汇总数据
|
||||
*/
|
||||
DeviceTrafficTrendRespVO getAreaTrafficTrend(Long areaId, LocalDate startDate, LocalDate endDate);
|
||||
|
||||
/**
|
||||
* 按区域ID列表查实时客流(汇总)
|
||||
*
|
||||
* @param areaIds 区域ID列表
|
||||
* @return 汇总的实时客流数据
|
||||
*/
|
||||
TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds);
|
||||
|
||||
/**
|
||||
* 按区域ID列表查趋势(汇总)
|
||||
*
|
||||
* @param areaIds 区域ID列表
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return 区域客流趋势汇总数据
|
||||
*/
|
||||
DeviceTrafficTrendRespVO getAreaTrafficTrend(List<Long> areaIds, LocalDate startDate, LocalDate endDate);
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.*;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DashboardStatsRespVO.*;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DeviceTrafficRealtimeRespVO;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DeviceTrafficTrendRespVO;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficRealtimeRespVO.AreaTrafficItem;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficRealtimeRespVO.HourlyTrend;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.WorkspaceStatsRespVO.UrgentTaskItem;
|
||||
@@ -155,6 +157,14 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
|
||||
|
||||
// 查询昨日数据(用于昨日小时趋势对比)
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
|
||||
List<OpsTrafficStatisticsDO> yesterdayStats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, todayStart));
|
||||
|
||||
// 加载区域名称映射
|
||||
Map<Long, String> areaNameMap = loadAreaNameMap();
|
||||
|
||||
@@ -186,8 +196,9 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.build());
|
||||
}
|
||||
|
||||
// 小时趋势
|
||||
// 小时趋势(今日 + 昨日)
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(todayStats);
|
||||
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
|
||||
|
||||
return TrafficRealtimeRespVO.builder()
|
||||
.totalIn(totalIn)
|
||||
@@ -195,6 +206,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.currentOccupancy(Math.max(0, totalIn - totalOut))
|
||||
.areas(areas)
|
||||
.hourlyTrend(hourlyTrend)
|
||||
.yesterdayHourlyTrend(yesterdayHourlyTrend)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -364,6 +376,244 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:device-traffic#5m", key = "#deviceId", unless = "#result == null")
|
||||
public DeviceTrafficRealtimeRespVO getDeviceTrafficRealtime(Long deviceId) {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDateTime todayStart = today.atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
|
||||
List<OpsTrafficStatisticsDO> stats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.eq(OpsTrafficStatisticsDO::getDeviceId, deviceId)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
|
||||
|
||||
// 查询昨日数据
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
|
||||
List<OpsTrafficStatisticsDO> yesterdayStats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.eq(OpsTrafficStatisticsDO::getDeviceId, deviceId)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, todayStart));
|
||||
|
||||
long totalIn = stats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
|
||||
long totalOut = stats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
|
||||
|
||||
// 获取区域信息
|
||||
Long areaId = stats.stream().map(OpsTrafficStatisticsDO::getAreaId).filter(a -> a != null).findFirst().orElse(null);
|
||||
Map<Long, String> areaNameMap = loadAreaNameMap();
|
||||
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(stats);
|
||||
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
|
||||
|
||||
return DeviceTrafficRealtimeRespVO.builder()
|
||||
.deviceId(deviceId)
|
||||
.areaId(areaId)
|
||||
.areaName(areaId != null ? areaNameMap.getOrDefault(areaId, "未知区域") : null)
|
||||
.todayIn(totalIn)
|
||||
.todayOut(totalOut)
|
||||
.currentOccupancy(Math.max(0, totalIn - totalOut))
|
||||
.hourlyTrend(hourlyTrend)
|
||||
.yesterdayHourlyTrend(yesterdayHourlyTrend)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:area-device-traffic#5m", key = "#areaId", unless = "#result == null")
|
||||
public List<DeviceTrafficRealtimeRespVO> getAreaDeviceTrafficRealtime(Long areaId) {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDateTime todayStart = today.atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
|
||||
List<OpsTrafficStatisticsDO> stats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.eq(OpsTrafficStatisticsDO::getAreaId, areaId)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
|
||||
|
||||
// 查询昨日数据
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
|
||||
List<OpsTrafficStatisticsDO> yesterdayStats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.eq(OpsTrafficStatisticsDO::getAreaId, areaId)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, todayStart));
|
||||
|
||||
Map<Long, String> areaNameMap = loadAreaNameMap();
|
||||
String areaName = areaNameMap.getOrDefault(areaId, "未知区域");
|
||||
|
||||
// 按设备分组(今日)
|
||||
Map<Long, List<OpsTrafficStatisticsDO>> byDevice = stats.stream()
|
||||
.filter(s -> s.getDeviceId() != null)
|
||||
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getDeviceId));
|
||||
|
||||
// 按设备分组(昨日)
|
||||
Map<Long, List<OpsTrafficStatisticsDO>> yesterdayByDevice = yesterdayStats.stream()
|
||||
.filter(s -> s.getDeviceId() != null)
|
||||
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getDeviceId));
|
||||
|
||||
List<DeviceTrafficRealtimeRespVO> result = new ArrayList<>();
|
||||
for (Map.Entry<Long, List<OpsTrafficStatisticsDO>> entry : byDevice.entrySet()) {
|
||||
Long deviceId = entry.getKey();
|
||||
List<OpsTrafficStatisticsDO> deviceStats = entry.getValue();
|
||||
|
||||
long deviceIn = deviceStats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
|
||||
long deviceOut = deviceStats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
|
||||
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(deviceStats);
|
||||
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(
|
||||
yesterdayByDevice.getOrDefault(deviceId, List.of()));
|
||||
|
||||
result.add(DeviceTrafficRealtimeRespVO.builder()
|
||||
.deviceId(deviceId)
|
||||
.areaId(areaId)
|
||||
.areaName(areaName)
|
||||
.todayIn(deviceIn)
|
||||
.todayOut(deviceOut)
|
||||
.currentOccupancy(Math.max(0, deviceIn - deviceOut))
|
||||
.hourlyTrend(hourlyTrend)
|
||||
.yesterdayHourlyTrend(yesterdayHourlyTrend)
|
||||
.build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:device-traffic-trend#5m",
|
||||
key = "#deviceId + ':' + #startDate + ':' + #endDate", unless = "#result == null")
|
||||
public DeviceTrafficTrendRespVO getDeviceTrafficTrend(Long deviceId, LocalDate startDate, LocalDate endDate) {
|
||||
List<OpsTrafficStatisticsDO> stats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.eq(OpsTrafficStatisticsDO::getDeviceId, deviceId)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, startDate.atStartOfDay())
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, endDate.plusDays(1).atStartOfDay()));
|
||||
|
||||
return buildDeviceTrafficTrendFromStats(stats, deviceId, null, startDate, endDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:area-traffic-trend#5m",
|
||||
key = "#areaId + ':' + #startDate + ':' + #endDate", unless = "#result == null")
|
||||
public DeviceTrafficTrendRespVO getAreaTrafficTrend(Long areaId, LocalDate startDate, LocalDate endDate) {
|
||||
List<OpsTrafficStatisticsDO> stats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.eq(OpsTrafficStatisticsDO::getAreaId, areaId)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, startDate.atStartOfDay())
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, endDate.plusDays(1).atStartOfDay()));
|
||||
|
||||
return buildDeviceTrafficTrendFromStats(stats, null, areaId, startDate, endDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:area-traffic-realtime#5m",
|
||||
key = "T(String).join(',', #areaIds.![toString()])", unless = "#result == null")
|
||||
public TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds) {
|
||||
if (areaIds == null || areaIds.isEmpty()) {
|
||||
return TrafficRealtimeRespVO.builder()
|
||||
.totalIn(0L).totalOut(0L).currentOccupancy(0L)
|
||||
.message("该区域暂未配置客流设备")
|
||||
.build();
|
||||
}
|
||||
// 限制 areaIds 长度,防止 IN 查询过大
|
||||
if (areaIds.size() > 200) {
|
||||
areaIds = areaIds.subList(0, 200);
|
||||
}
|
||||
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDateTime todayStart = today.atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
|
||||
// 查询今日数据,条件 areaId IN (areaIds)
|
||||
List<OpsTrafficStatisticsDO> todayStats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.in(OpsTrafficStatisticsDO::getAreaId, areaIds)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
|
||||
|
||||
// 查询昨日数据
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
|
||||
List<OpsTrafficStatisticsDO> yesterdayStats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.in(OpsTrafficStatisticsDO::getAreaId, areaIds)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, todayStart));
|
||||
|
||||
// 无数据时设置提示信息
|
||||
if (todayStats.isEmpty() && yesterdayStats.isEmpty()) {
|
||||
return TrafficRealtimeRespVO.builder()
|
||||
.totalIn(0L).totalOut(0L).currentOccupancy(0L)
|
||||
.message("该区域暂未配置客流设备")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 汇总 totalIn、totalOut
|
||||
long totalIn = todayStats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
|
||||
long totalOut = todayStats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
|
||||
|
||||
// 按区域汇总明细
|
||||
Map<Long, String> areaNameMap = loadAreaNameMap();
|
||||
Map<Long, List<OpsTrafficStatisticsDO>> byArea = todayStats.stream()
|
||||
.filter(s -> s.getAreaId() != null)
|
||||
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getAreaId));
|
||||
|
||||
List<AreaTrafficItem> areas = new ArrayList<>();
|
||||
for (Map.Entry<Long, List<OpsTrafficStatisticsDO>> entry : byArea.entrySet()) {
|
||||
Long areaId = entry.getKey();
|
||||
List<OpsTrafficStatisticsDO> stats = entry.getValue();
|
||||
long areaIn = stats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
|
||||
long areaOut = stats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
|
||||
int deviceCount = (int) stats.stream().map(OpsTrafficStatisticsDO::getDeviceId).distinct().count();
|
||||
|
||||
areas.add(AreaTrafficItem.builder()
|
||||
.areaId(areaId)
|
||||
.areaName(areaNameMap.getOrDefault(areaId, "未知区域"))
|
||||
.todayIn(areaIn)
|
||||
.todayOut(areaOut)
|
||||
.currentOccupancy(Math.max(0, areaIn - areaOut))
|
||||
.deviceCount(deviceCount)
|
||||
.build());
|
||||
}
|
||||
|
||||
// 小时趋势
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(todayStats);
|
||||
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
|
||||
|
||||
return TrafficRealtimeRespVO.builder()
|
||||
.totalIn(totalIn)
|
||||
.totalOut(totalOut)
|
||||
.currentOccupancy(Math.max(0, totalIn - totalOut))
|
||||
.areas(areas)
|
||||
.hourlyTrend(hourlyTrend)
|
||||
.yesterdayHourlyTrend(yesterdayHourlyTrend)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:area-traffic-trend-multi#5m",
|
||||
key = "T(String).join(',', #areaIds.![toString()]) + ':' + #startDate + ':' + #endDate",
|
||||
unless = "#result == null")
|
||||
public DeviceTrafficTrendRespVO getAreaTrafficTrend(List<Long> areaIds, LocalDate startDate, LocalDate endDate) {
|
||||
if (areaIds == null || areaIds.isEmpty()) {
|
||||
return DeviceTrafficTrendRespVO.builder().build();
|
||||
}
|
||||
// 限制 areaIds 长度
|
||||
if (areaIds.size() > 200) {
|
||||
areaIds = areaIds.subList(0, 200);
|
||||
}
|
||||
|
||||
List<OpsTrafficStatisticsDO> stats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.in(OpsTrafficStatisticsDO::getAreaId, areaIds)
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, startDate.atStartOfDay())
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, endDate.plusDays(1).atStartOfDay()));
|
||||
|
||||
return buildDeviceTrafficTrendFromStats(stats, null, null, startDate, endDate);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
private Integer countByStatusAndType(String status, String orderType) {
|
||||
@@ -694,12 +944,72 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载区域名称映射
|
||||
* 构建设备/区域维度的客流趋势数据
|
||||
*/
|
||||
private DeviceTrafficTrendRespVO buildDeviceTrafficTrendFromStats(
|
||||
List<OpsTrafficStatisticsDO> stats, Long deviceId, Long areaId,
|
||||
LocalDate startDate, LocalDate endDate) {
|
||||
Map<LocalDate, Long> dailyInMap = new HashMap<>();
|
||||
Map<LocalDate, Long> dailyOutMap = new HashMap<>();
|
||||
|
||||
for (OpsTrafficStatisticsDO stat : stats) {
|
||||
if (stat.getStatHour() != null) {
|
||||
LocalDate date = stat.getStatHour().toLocalDate();
|
||||
long peopleIn = stat.getPeopleIn() != null ? stat.getPeopleIn() : 0;
|
||||
long peopleOut = stat.getPeopleOut() != null ? stat.getPeopleOut() : 0;
|
||||
dailyInMap.merge(date, peopleIn, Long::sum);
|
||||
dailyOutMap.merge(date, peopleOut, Long::sum);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> dates = new ArrayList<>();
|
||||
List<Long> inData = new ArrayList<>();
|
||||
List<Long> outData = new ArrayList<>();
|
||||
List<Long> netData = new ArrayList<>();
|
||||
long totalIn = 0;
|
||||
long totalOut = 0;
|
||||
|
||||
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
|
||||
dates.add(d.format(DATE_FORMATTER));
|
||||
long dayIn = dailyInMap.getOrDefault(d, 0L);
|
||||
long dayOut = dailyOutMap.getOrDefault(d, 0L);
|
||||
inData.add(dayIn);
|
||||
outData.add(dayOut);
|
||||
netData.add(dayIn - dayOut);
|
||||
totalIn += dayIn;
|
||||
totalOut += dayOut;
|
||||
}
|
||||
|
||||
return DeviceTrafficTrendRespVO.builder()
|
||||
.deviceId(deviceId)
|
||||
.areaId(areaId)
|
||||
.dates(dates)
|
||||
.inData(inData)
|
||||
.outData(outData)
|
||||
.netData(netData)
|
||||
.totalIn(totalIn)
|
||||
.totalOut(totalOut)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载区域名称映射(带本地缓存,5分钟过期)
|
||||
*/
|
||||
private volatile Map<Long, String> areaNameMapCache;
|
||||
private volatile long areaNameMapCacheTime;
|
||||
private static final long AREA_NAME_CACHE_TTL = 5 * 60 * 1000L; // 5分钟
|
||||
|
||||
private Map<Long, String> loadAreaNameMap() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (areaNameMapCache != null && (now - areaNameMapCacheTime) < AREA_NAME_CACHE_TTL) {
|
||||
return areaNameMapCache;
|
||||
}
|
||||
List<OpsBusAreaDO> areas = opsBusAreaMapper.selectList(new LambdaQueryWrapper<>());
|
||||
return areas.stream()
|
||||
Map<Long, String> map = areas.stream()
|
||||
.collect(Collectors.toMap(OpsBusAreaDO::getId, OpsBusAreaDO::getAreaName, (a, b) -> a));
|
||||
areaNameMapCache = map;
|
||||
areaNameMapCacheTime = now;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user