refactor(ops): 重构统计模块,支持日期参数化查询及代码质量优化
- 客流接口支持指定日期查询(getTrafficRealtime、getTrafficTrend、getAreaTrafficRealtime) - 移除昨日对比趋势字段(yesterdayHourlyTrend),简化为单日期模式 - 漏斗图改为工单状态分布(FunnelItem→StatusDistributionItem),使用 SQL COUNT 替代内存分组 - 新增工牌队列统计(BadgeQueueStats),按 orderType 过滤避免跨类型数据混入 - 在线工牌计数仅统计 IDLE/BUSY 状态(排除 PAUSED/OFFLINE) - 修复通配符导入和全限定类名引用,规范化 import 语句 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,8 +36,8 @@ public class DashboardStatsRespVO {
|
||||
@Schema(description = "时效趋势数据")
|
||||
private TimeTrendData timeTrendData;
|
||||
|
||||
@Schema(description = "漏斗数据")
|
||||
private List<FunnelItem> funnelData;
|
||||
@Schema(description = "工单状态分布数据")
|
||||
private List<StatusDistributionItem> statusDistribution;
|
||||
|
||||
@Schema(description = "热力图数据(近7天)")
|
||||
private HeatmapData heatmapData;
|
||||
@@ -45,6 +45,9 @@ public class DashboardStatsRespVO {
|
||||
@Schema(description = "功能类型排行")
|
||||
private List<FunctionTypeRankingItem> functionTypeRanking;
|
||||
|
||||
@Schema(description = "工牌队列统计(近7天每天的排队数量)")
|
||||
private BadgeQueueStats badgeQueueStats;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@@ -86,8 +89,8 @@ public class DashboardStatsRespVO {
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class FunnelItem {
|
||||
@Schema(description = "阶段名称")
|
||||
public static class StatusDistributionItem {
|
||||
@Schema(description = "状态名称")
|
||||
private String name;
|
||||
@Schema(description = "数量")
|
||||
private Integer value;
|
||||
@@ -121,4 +124,15 @@ public class DashboardStatsRespVO {
|
||||
private Double rate;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class BadgeQueueStats {
|
||||
@Schema(description = "日期列表")
|
||||
private List<String> dates;
|
||||
@Schema(description = "每日排队工牌数量")
|
||||
private List<Integer> queueData;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ public class DeviceTrafficRealtimeRespVO {
|
||||
@Schema(description = "当前在场人数")
|
||||
private Long currentOccupancy;
|
||||
|
||||
@Schema(description = "今日小时趋势")
|
||||
@Schema(description = "统计日期")
|
||||
private String statDate;
|
||||
|
||||
@Schema(description = "小时趋势")
|
||||
private TrafficRealtimeRespVO.HourlyTrend hourlyTrend;
|
||||
|
||||
@Schema(description = "昨日小时趋势")
|
||||
private TrafficRealtimeRespVO.HourlyTrend yesterdayHourlyTrend;
|
||||
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ public class TrafficRealtimeRespVO {
|
||||
@Schema(description = "区域客流明细")
|
||||
private List<AreaTrafficItem> areas;
|
||||
|
||||
@Schema(description = "今日小时趋势")
|
||||
private HourlyTrend hourlyTrend;
|
||||
@Schema(description = "统计日期")
|
||||
private String statDate;
|
||||
|
||||
@Schema(description = "昨日小时趋势")
|
||||
private HourlyTrend yesterdayHourlyTrend;
|
||||
@Schema(description = "小时趋势")
|
||||
private HourlyTrend hourlyTrend;
|
||||
|
||||
@Schema(description = "提示信息,如区域暂未配置客流设备")
|
||||
private String message;
|
||||
|
||||
@@ -13,14 +13,14 @@ import java.util.List;
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Schema(description = "管理后台 - 近7天客流统计 Response VO")
|
||||
@Schema(description = "管理后台 - 客流趋势统计 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TrafficTrendRespVO {
|
||||
|
||||
@Schema(description = "日期列表(近7天)")
|
||||
@Schema(description = "日期列表")
|
||||
private List<String> dates;
|
||||
|
||||
@Schema(description = "每日进入人数")
|
||||
|
||||
@@ -30,9 +30,10 @@ public interface OpsStatisticsService {
|
||||
/**
|
||||
* 获取实时客流监测数据
|
||||
*
|
||||
* @param date 指定日期(可选,默认今天)
|
||||
* @return 实时客流数据
|
||||
*/
|
||||
TrafficRealtimeRespVO getTrafficRealtime();
|
||||
TrafficRealtimeRespVO getTrafficRealtime(LocalDate date);
|
||||
|
||||
/**
|
||||
* 获取工作台统计数据
|
||||
@@ -42,11 +43,13 @@ public interface OpsStatisticsService {
|
||||
WorkspaceStatsRespVO getWorkspaceStats();
|
||||
|
||||
/**
|
||||
* 获取近7天客流趋势统计
|
||||
* 获取客流趋势统计
|
||||
*
|
||||
* @return 近7天客流趋势数据
|
||||
* @param startDate 开始日期(可选,默认近7天)
|
||||
* @param endDate 结束日期(可选,默认今天)
|
||||
* @return 客流趋势数据
|
||||
*/
|
||||
TrafficTrendRespVO getTrafficTrend();
|
||||
TrafficTrendRespVO getTrafficTrend(LocalDate startDate, LocalDate endDate);
|
||||
|
||||
/**
|
||||
* 按设备查实时客流
|
||||
@@ -88,9 +91,10 @@ public interface OpsStatisticsService {
|
||||
* 按区域ID列表查实时客流(汇总)
|
||||
*
|
||||
* @param areaIds 区域ID列表
|
||||
* @param date 指定日期(可选,默认今天)
|
||||
* @return 汇总的实时客流数据
|
||||
*/
|
||||
TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds);
|
||||
TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds, LocalDate date);
|
||||
|
||||
/**
|
||||
* 按区域ID列表查趋势(汇总)
|
||||
|
||||
@@ -15,6 +15,8 @@ import com.viewsh.module.ops.dal.dataobject.statistics.OpsTrafficStatisticsDO;
|
||||
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.statistics.OpsTrafficStatisticsMapper;
|
||||
import com.viewsh.module.ops.dal.dataobject.queue.OpsOrderQueueDO;
|
||||
import com.viewsh.module.ops.dal.mysql.queue.OpsOrderQueueMapper;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -28,8 +30,10 @@ import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -70,6 +74,9 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
@Resource
|
||||
private OpsTrafficStatisticsMapper trafficStatisticsMapper;
|
||||
|
||||
@Resource
|
||||
private OpsOrderQueueMapper opsOrderQueueMapper;
|
||||
|
||||
/**
|
||||
* 在线员工数提供者(由 ops-server 层注入,从 environment-biz 获取工牌状态)
|
||||
* 返回 [onlineCount, totalCount]
|
||||
@@ -118,18 +125,23 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
// 7. 时效趋势(单次 GROUP BY 查询)
|
||||
TimeTrendData timeTrendData = buildTimeTrendData(orderType, startDate, endDate);
|
||||
|
||||
// 8. 漏斗数据(基于指定时间范围的工单状态推算)
|
||||
List<FunnelItem> funnelData = buildFunnelData(orderType, startDateTime, endDateTime);
|
||||
|
||||
// 9. 热力图(近7天)
|
||||
// 近7天时间范围(多个图表复用)
|
||||
LocalDate sevenDaysAgo = LocalDate.now().minusDays(6); // 包含今天共7天
|
||||
LocalDateTime sevenDaysAgoStart = sevenDaysAgo.atStartOfDay();
|
||||
LocalDateTime nowEnd = LocalDate.now().plusDays(1).atStartOfDay();
|
||||
|
||||
// 8. 工单状态分布(近7天所有工单的状态占比)
|
||||
List<StatusDistributionItem> statusDistribution = buildStatusDistribution(orderType, sevenDaysAgoStart, nowEnd);
|
||||
|
||||
// 9. 热力图(近7天)
|
||||
HeatmapData heatmapData = buildHeatmapData(orderType, sevenDaysAgoStart, nowEnd);
|
||||
|
||||
// 10. 功能类型排行(单次 GROUP BY 查询)
|
||||
List<FunctionTypeRankingItem> functionTypeRanking = buildFunctionTypeRanking(orderType);
|
||||
|
||||
// 11. 工牌队列统计(近7天每天排队的工单数 = QUEUED 状态)
|
||||
BadgeQueueStats badgeQueueStats = buildBadgeQueueStats(orderType, sevenDaysAgo, LocalDate.now());
|
||||
|
||||
return DashboardStatsRespVO.builder()
|
||||
.pendingCount(pendingCount)
|
||||
.inProgressCount(inProgressCount)
|
||||
@@ -138,38 +150,33 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.trendData(trendData)
|
||||
.hourlyDistribution(hourlyDistribution)
|
||||
.timeTrendData(timeTrendData)
|
||||
.funnelData(funnelData)
|
||||
.statusDistribution(statusDistribution)
|
||||
.heatmapData(heatmapData)
|
||||
.functionTypeRanking(functionTypeRanking)
|
||||
.badgeQueueStats(badgeQueueStats)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:traffic#5m", unless = "#result == null")
|
||||
public TrafficRealtimeRespVO getTrafficRealtime() {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDateTime todayStart = today.atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
@Cacheable(value = "ops:statistics:traffic#5m", key = "#date != null ? #date.toString() : T(java.time.LocalDate).now().toString()", unless = "#result == null")
|
||||
public TrafficRealtimeRespVO getTrafficRealtime(LocalDate date) {
|
||||
if (date == null) {
|
||||
date = LocalDate.now();
|
||||
}
|
||||
LocalDateTime dayStart = date.atStartOfDay();
|
||||
LocalDateTime dayEnd = dayStart.plusDays(1);
|
||||
|
||||
// 从 ops_traffic_statistics 表查询今日汇总(使用 ge + lt 避免边界重叠)
|
||||
List<OpsTrafficStatisticsDO> todayStats = trafficStatisticsMapper.selectList(
|
||||
// 从 ops_traffic_statistics 表查询指定日期汇总(使用 ge + lt 避免边界重叠)
|
||||
List<OpsTrafficStatisticsDO> dayStats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
.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));
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, dayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, dayEnd));
|
||||
|
||||
// 加载区域名称映射
|
||||
Map<Long, String> areaNameMap = loadAreaNameMap();
|
||||
|
||||
// 按区域汇总
|
||||
Map<Long, List<OpsTrafficStatisticsDO>> byArea = todayStats.stream()
|
||||
Map<Long, List<OpsTrafficStatisticsDO>> byArea = dayStats.stream()
|
||||
.filter(s -> s.getAreaId() != null)
|
||||
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getAreaId));
|
||||
|
||||
@@ -196,17 +203,16 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.build());
|
||||
}
|
||||
|
||||
// 小时趋势(今日 + 昨日)
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(todayStats);
|
||||
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
|
||||
// 小时趋势
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(dayStats);
|
||||
|
||||
return TrafficRealtimeRespVO.builder()
|
||||
.totalIn(totalIn)
|
||||
.totalOut(totalOut)
|
||||
.currentOccupancy(Math.max(0, totalIn - totalOut))
|
||||
.areas(areas)
|
||||
.statDate(date.toString())
|
||||
.hourlyTrend(hourlyTrend)
|
||||
.yesterdayHourlyTrend(yesterdayHourlyTrend)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -231,8 +237,11 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 待处理工单数
|
||||
Integer pendingCount = countByStatusAndType("PENDING", orderType);
|
||||
// 2. 待处理工单数(所有未取消、未完成的工单)
|
||||
Integer pendingCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getOrderType, orderType)
|
||||
.notIn(OpsOrderDO::getStatus, "COMPLETED", "CANCELLED")));
|
||||
|
||||
// 3. 平均响应时长(今日已完成的工单,使用 ge + lt 避免边界重叠)
|
||||
List<OpsOrderDO> completedToday = opsOrderMapper.selectList(
|
||||
@@ -314,14 +323,24 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:traffic-trend#5m", unless = "#result == null")
|
||||
public TrafficTrendRespVO getTrafficTrend() {
|
||||
@Cacheable(value = "ops:statistics:traffic-trend#5m",
|
||||
key = "(#startDate != null ? #startDate.toString() : 'default') + ':' + (#endDate != null ? #endDate.toString() : 'default')", unless = "#result == null")
|
||||
public TrafficTrendRespVO getTrafficTrend(LocalDate startDate, LocalDate endDate) {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate sevenDaysAgo = today.minusDays(6); // 包含今天共7天
|
||||
if (startDate == null) {
|
||||
startDate = today.minusDays(6);
|
||||
}
|
||||
if (endDate == null) {
|
||||
endDate = today;
|
||||
}
|
||||
// 限制最大范围为31天
|
||||
if (java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) > 30) {
|
||||
startDate = endDate.minusDays(30);
|
||||
}
|
||||
|
||||
// 从 ops_traffic_statistics 表查询近7天的客流数据
|
||||
LocalDateTime startDateTime = sevenDaysAgo.atStartOfDay();
|
||||
LocalDateTime endDateTime = today.plusDays(1).atStartOfDay();
|
||||
// 从 ops_traffic_statistics 表查询客流数据
|
||||
LocalDateTime startDateTime = startDate.atStartOfDay();
|
||||
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
|
||||
|
||||
List<OpsTrafficStatisticsDO> stats = trafficStatisticsMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||
@@ -352,7 +371,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
long totalIn = 0;
|
||||
long totalOut = 0;
|
||||
|
||||
for (LocalDate d = sevenDaysAgo; !d.isAfter(today); d = d.plusDays(1)) {
|
||||
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);
|
||||
@@ -389,15 +408,6 @@ 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>()
|
||||
.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();
|
||||
|
||||
@@ -406,7 +416,6 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
Map<Long, String> areaNameMap = loadAreaNameMap();
|
||||
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(stats);
|
||||
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
|
||||
|
||||
return DeviceTrafficRealtimeRespVO.builder()
|
||||
.deviceId(deviceId)
|
||||
@@ -415,8 +424,8 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.todayIn(totalIn)
|
||||
.todayOut(totalOut)
|
||||
.currentOccupancy(Math.max(0, totalIn - totalOut))
|
||||
.statDate(today.toString())
|
||||
.hourlyTrend(hourlyTrend)
|
||||
.yesterdayHourlyTrend(yesterdayHourlyTrend)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -433,28 +442,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>()
|
||||
.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();
|
||||
@@ -464,8 +459,6 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
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)
|
||||
@@ -474,8 +467,8 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.todayIn(deviceIn)
|
||||
.todayOut(deviceOut)
|
||||
.currentOccupancy(Math.max(0, deviceIn - deviceOut))
|
||||
.statDate(today.toString())
|
||||
.hourlyTrend(hourlyTrend)
|
||||
.yesterdayHourlyTrend(yesterdayHourlyTrend)
|
||||
.build());
|
||||
}
|
||||
return result;
|
||||
@@ -509,8 +502,8 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "ops:statistics:area-traffic-realtime#5m",
|
||||
key = "T(String).join(',', #areaIds.![toString()])", unless = "#result == null")
|
||||
public TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds) {
|
||||
key = "T(String).join(',', #areaIds.![toString()]) + ':' + (#date != null ? #date.toString() : T(java.time.LocalDate).now().toString())", unless = "#result == null")
|
||||
public TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds, LocalDate date) {
|
||||
if (areaIds == null || areaIds.isEmpty()) {
|
||||
return TrafficRealtimeRespVO.builder()
|
||||
.totalIn(0L).totalOut(0L).currentOccupancy(0L)
|
||||
@@ -521,42 +514,36 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
if (areaIds.size() > 200) {
|
||||
areaIds = areaIds.subList(0, 200);
|
||||
}
|
||||
if (date == null) {
|
||||
date = LocalDate.now();
|
||||
}
|
||||
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDateTime todayStart = today.atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
LocalDateTime dayStart = date.atStartOfDay();
|
||||
LocalDateTime dayEnd = dayStart.plusDays(1);
|
||||
|
||||
// 查询今日数据,条件 areaId IN (areaIds)
|
||||
List<OpsTrafficStatisticsDO> todayStats = trafficStatisticsMapper.selectList(
|
||||
// 查询指定日期数据,条件 areaId IN (areaIds)
|
||||
List<OpsTrafficStatisticsDO> dayStats = 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));
|
||||
.ge(OpsTrafficStatisticsDO::getStatHour, dayStart)
|
||||
.lt(OpsTrafficStatisticsDO::getStatHour, dayEnd));
|
||||
|
||||
// 无数据时设置提示信息
|
||||
if (todayStats.isEmpty() && yesterdayStats.isEmpty()) {
|
||||
if (dayStats.isEmpty()) {
|
||||
return TrafficRealtimeRespVO.builder()
|
||||
.totalIn(0L).totalOut(0L).currentOccupancy(0L)
|
||||
.statDate(date.toString())
|
||||
.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();
|
||||
long totalIn = dayStats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
|
||||
long totalOut = dayStats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
|
||||
|
||||
// 按区域汇总明细
|
||||
Map<Long, String> areaNameMap = loadAreaNameMap();
|
||||
Map<Long, List<OpsTrafficStatisticsDO>> byArea = todayStats.stream()
|
||||
Map<Long, List<OpsTrafficStatisticsDO>> byArea = dayStats.stream()
|
||||
.filter(s -> s.getAreaId() != null)
|
||||
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getAreaId));
|
||||
|
||||
@@ -579,16 +566,15 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
}
|
||||
|
||||
// 小时趋势
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(todayStats);
|
||||
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
|
||||
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(dayStats);
|
||||
|
||||
return TrafficRealtimeRespVO.builder()
|
||||
.totalIn(totalIn)
|
||||
.totalOut(totalOut)
|
||||
.currentOccupancy(Math.max(0, totalIn - totalOut))
|
||||
.areas(areas)
|
||||
.statDate(date.toString())
|
||||
.hourlyTrend(hourlyTrend)
|
||||
.yesterdayHourlyTrend(yesterdayHourlyTrend)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -743,48 +729,42 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建漏斗数据(统计指定时间范围内的工单)
|
||||
* 构建工单状态分布数据 -- SQL GROUP BY 聚合,避免大量数据加载到内存
|
||||
*/
|
||||
private List<FunnelItem> buildFunnelData(String orderType, LocalDateTime startDateTime, LocalDateTime endDateTime) {
|
||||
// 创建工单 = 时间范围内创建的所有非取消工单
|
||||
long totalCreated = opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getOrderType, orderType)
|
||||
.ne(OpsOrderDO::getStatus, "CANCELLED")
|
||||
.ge(OpsOrderDO::getCreateTime, startDateTime)
|
||||
.lt(OpsOrderDO::getCreateTime, endDateTime));
|
||||
private List<StatusDistributionItem> buildStatusDistribution(String orderType,
|
||||
LocalDateTime startDateTime,
|
||||
LocalDateTime endDateTime) {
|
||||
Map<String, String> statusLabelMap = new LinkedHashMap<>();
|
||||
statusLabelMap.put("PENDING", "待处理");
|
||||
statusLabelMap.put("QUEUED", "排队中");
|
||||
statusLabelMap.put("DISPATCHED", "已派单");
|
||||
statusLabelMap.put("ARRIVED", "已到岗");
|
||||
statusLabelMap.put("COMPLETED", "已完成");
|
||||
statusLabelMap.put("CANCELLED", "已取消");
|
||||
statusLabelMap.put("PAUSED", "已暂停");
|
||||
|
||||
// 已分配 = 时间范围内创建且已进入工作流程的工单(排除 PENDING 和 CANCELLED)
|
||||
long assigned = opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getOrderType, orderType)
|
||||
.notIn(OpsOrderDO::getStatus, "PENDING", "CANCELLED")
|
||||
.ge(OpsOrderDO::getCreateTime, startDateTime)
|
||||
.lt(OpsOrderDO::getCreateTime, endDateTime));
|
||||
// 使用 selectCount 按状态逐个查询(状态数量有限,最多7次查询,避免大量数据加载到内存)
|
||||
Map<String, Long> statusCountMap = new HashMap<>();
|
||||
for (String status : statusLabelMap.keySet()) {
|
||||
Long count = opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getOrderType, orderType)
|
||||
.eq(OpsOrderDO::getStatus, status)
|
||||
.ge(OpsOrderDO::getCreateTime, startDateTime)
|
||||
.lt(OpsOrderDO::getCreateTime, endDateTime));
|
||||
if (count > 0) {
|
||||
statusCountMap.put(status, count);
|
||||
}
|
||||
}
|
||||
|
||||
// 已到岗 = 时间范围内创建且已到岗的工单(ARRIVED + COMPLETED + PAUSED)
|
||||
long arrived = opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getOrderType, orderType)
|
||||
.in(OpsOrderDO::getStatus, "ARRIVED", "COMPLETED", "PAUSED")
|
||||
.ge(OpsOrderDO::getCreateTime, startDateTime)
|
||||
.lt(OpsOrderDO::getCreateTime, endDateTime));
|
||||
|
||||
// 已完成 = 时间范围内创建且已完成的工单
|
||||
long completed = opsOrderMapper.selectCount(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.eq(OpsOrderDO::getStatus, "COMPLETED")
|
||||
.eq(OpsOrderDO::getOrderType, orderType)
|
||||
.ge(OpsOrderDO::getCreateTime, startDateTime)
|
||||
.lt(OpsOrderDO::getCreateTime, endDateTime));
|
||||
|
||||
List<FunnelItem> funnelData = new ArrayList<>();
|
||||
funnelData.add(FunnelItem.builder().name("创建工单").value((int) totalCreated).build());
|
||||
funnelData.add(FunnelItem.builder().name("已分配").value((int) assigned).build());
|
||||
funnelData.add(FunnelItem.builder().name("已到岗").value((int) arrived).build());
|
||||
funnelData.add(FunnelItem.builder().name("已完成").value((int) completed).build());
|
||||
|
||||
return funnelData;
|
||||
List<StatusDistributionItem> result = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : statusLabelMap.entrySet()) {
|
||||
Long count = statusCountMap.get(entry.getKey());
|
||||
if (count != null && count > 0) {
|
||||
result.add(StatusDistributionItem.builder().name(entry.getValue()).value(count.intValue()).build());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -883,6 +863,61 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工牌队列统计(近7天每天入队的工单数,查 ops_order_queue 表)
|
||||
* 通过关联 ops_order 表按 orderType 过滤
|
||||
*/
|
||||
private BadgeQueueStats buildBadgeQueueStats(String orderType, LocalDate startDate, LocalDate endDate) {
|
||||
LocalDateTime startDateTime = startDate.atStartOfDay();
|
||||
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
|
||||
|
||||
// 先查询该类型在时间范围内的工单ID集合
|
||||
List<OpsOrderDO> orders = opsOrderMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||
.select(OpsOrderDO::getId)
|
||||
.eq(OpsOrderDO::getOrderType, orderType)
|
||||
.ge(OpsOrderDO::getCreateTime, startDateTime)
|
||||
.lt(OpsOrderDO::getCreateTime, endDateTime));
|
||||
Set<Long> orderIds = orders.stream()
|
||||
.map(OpsOrderDO::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (orderIds.isEmpty()) {
|
||||
// 无工单,直接返回空数据
|
||||
List<String> dates = new ArrayList<>();
|
||||
List<Integer> queueData = new ArrayList<>();
|
||||
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
|
||||
dates.add(d.format(DATE_FORMATTER));
|
||||
queueData.add(0);
|
||||
}
|
||||
return BadgeQueueStats.builder().dates(dates).queueData(queueData).build();
|
||||
}
|
||||
|
||||
// 查询队列记录,按工单ID过滤
|
||||
List<OpsOrderQueueDO> queueRecords =
|
||||
opsOrderQueueMapper.selectList(
|
||||
new LambdaQueryWrapperX<OpsOrderQueueDO>()
|
||||
.in(OpsOrderQueueDO::getOpsOrderId, orderIds)
|
||||
.ge(OpsOrderQueueDO::getEnqueueTime, startDateTime)
|
||||
.lt(OpsOrderQueueDO::getEnqueueTime, endDateTime));
|
||||
|
||||
Map<String, Integer> dailyQueueMap = queueRecords.stream()
|
||||
.filter(q -> q.getEnqueueTime() != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
q -> q.getEnqueueTime().toLocalDate().toString(),
|
||||
Collectors.summingInt(q -> 1)
|
||||
));
|
||||
|
||||
List<String> dates = new ArrayList<>();
|
||||
List<Integer> queueData = new ArrayList<>();
|
||||
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
|
||||
dates.add(d.format(DATE_FORMATTER));
|
||||
queueData.add(dailyQueueMap.getOrDefault(d.toString(), 0));
|
||||
}
|
||||
|
||||
return BadgeQueueStats.builder().dates(dates).queueData(queueData).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建客流小时趋势
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.viewsh.module.ops.config;
|
||||
|
||||
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
|
||||
import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO;
|
||||
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
|
||||
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
|
||||
import com.viewsh.module.ops.service.area.AreaDeviceService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -32,7 +33,11 @@ public class StatisticsConfiguration {
|
||||
return new int[]{0, 0};
|
||||
}
|
||||
List<BadgeDeviceStatusDTO> activeBadges = badgeDeviceStatusService.listActiveBadges();
|
||||
int onlineCount = activeBadges.size();
|
||||
// 只计算 IDLE 和 BUSY 状态的工牌作为在线数(不含 PAUSED 和 OFFLINE)
|
||||
int onlineCount = (int) activeBadges.stream()
|
||||
.filter(b -> b.getStatus() == BadgeDeviceStatusEnum.IDLE
|
||||
|| b.getStatus() == BadgeDeviceStatusEnum.BUSY)
|
||||
.count();
|
||||
// 从区域设备关联表查询所有已注册的工牌设备总数
|
||||
int totalCount = onlineCount;
|
||||
if (areaDeviceService != null) {
|
||||
|
||||
@@ -42,23 +42,26 @@ public class OpsTrafficController {
|
||||
@GetMapping("/realtime")
|
||||
@Operation(summary = "全局实时客流监测")
|
||||
@PreAuthorize("@ss.hasPermission('ops:traffic:query')")
|
||||
public CommonResult<TrafficRealtimeRespVO> getTrafficRealtime() {
|
||||
public CommonResult<TrafficRealtimeRespVO> getTrafficRealtime(
|
||||
@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
|
||||
if (opsStatisticsService == null) {
|
||||
log.warn("[getTrafficRealtime] OpsStatisticsService 未注入,返回默认值");
|
||||
return success(TrafficRealtimeRespVO.builder().build());
|
||||
}
|
||||
return success(opsStatisticsService.getTrafficRealtime());
|
||||
return success(opsStatisticsService.getTrafficRealtime(date));
|
||||
}
|
||||
|
||||
@GetMapping("/trend")
|
||||
@Operation(summary = "全局近7天客流趋势统计")
|
||||
@Operation(summary = "客流趋势统计")
|
||||
@PreAuthorize("@ss.hasPermission('ops:traffic:query')")
|
||||
public CommonResult<TrafficTrendRespVO> getTrafficTrend() {
|
||||
public CommonResult<TrafficTrendRespVO> getTrafficTrend(
|
||||
@RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
|
||||
if (opsStatisticsService == null) {
|
||||
log.warn("[getTrafficTrend] OpsStatisticsService 未注入,返回默认值");
|
||||
return success(TrafficTrendRespVO.builder().build());
|
||||
}
|
||||
return success(opsStatisticsService.getTrafficTrend());
|
||||
return success(opsStatisticsService.getTrafficTrend(startDate, endDate));
|
||||
}
|
||||
|
||||
@GetMapping("/device/realtime")
|
||||
@@ -79,7 +82,8 @@ public class OpsTrafficController {
|
||||
@Parameter(name = "areaIds", description = "区域ID列表", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('ops:traffic:query')")
|
||||
public CommonResult<TrafficRealtimeRespVO> getAreaTrafficRealtime(
|
||||
@RequestParam("areaIds") List<Long> areaIds) {
|
||||
@RequestParam("areaIds") List<Long> areaIds,
|
||||
@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
|
||||
if (opsStatisticsService == null) {
|
||||
log.warn("[getAreaTrafficRealtime] OpsStatisticsService 未注入,返回默认值");
|
||||
return success(TrafficRealtimeRespVO.builder().build());
|
||||
@@ -87,7 +91,7 @@ public class OpsTrafficController {
|
||||
if (areaIds.size() > 200) {
|
||||
areaIds = areaIds.subList(0, 200);
|
||||
}
|
||||
return success(opsStatisticsService.getAreaTrafficRealtime(areaIds));
|
||||
return success(opsStatisticsService.getAreaTrafficRealtime(areaIds, date));
|
||||
}
|
||||
|
||||
@GetMapping("/device/trend")
|
||||
|
||||
Reference in New Issue
Block a user