From 1c8eee9db460cf7c5d8b5adf32caef92e1d1c6f5 Mon Sep 17 00:00:00 2001 From: lzh Date: Sun, 1 Mar 2026 00:17:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ops):=20=E9=87=8D=E6=9E=84=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=A8=A1=E5=9D=97=EF=BC=8C=E6=94=AF=E6=8C=81=E6=97=A5?= =?UTF-8?q?=E6=9C=9F=E5=8F=82=E6=95=B0=E5=8C=96=E6=9F=A5=E8=AF=A2=E5=8F=8A?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 客流接口支持指定日期查询(getTrafficRealtime、getTrafficTrend、getAreaTrafficRealtime) - 移除昨日对比趋势字段(yesterdayHourlyTrend),简化为单日期模式 - 漏斗图改为工单状态分布(FunnelItem→StatusDistributionItem),使用 SQL COUNT 替代内存分组 - 新增工牌队列统计(BadgeQueueStats),按 orderType 过滤避免跨类型数据混入 - 在线工牌计数仅统计 IDLE/BUSY 状态(排除 PAUSED/OFFLINE) - 修复通配符导入和全限定类名引用,规范化 import 语句 Co-Authored-By: Claude Opus 4.6 --- .../vo/statistics/DashboardStatsRespVO.java | 22 +- .../DeviceTrafficRealtimeRespVO.java | 8 +- .../vo/statistics/TrafficRealtimeRespVO.java | 8 +- .../vo/statistics/TrafficTrendRespVO.java | 4 +- .../statistics/OpsStatisticsService.java | 14 +- .../statistics/OpsStatisticsServiceImpl.java | 293 ++++++++++-------- .../ops/config/StatisticsConfiguration.java | 7 +- .../admin/traffic/OpsTrafficController.java | 18 +- 8 files changed, 218 insertions(+), 156 deletions(-) diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DashboardStatsRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DashboardStatsRespVO.java index a3abd95..6ddc7af 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DashboardStatsRespVO.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DashboardStatsRespVO.java @@ -36,8 +36,8 @@ public class DashboardStatsRespVO { @Schema(description = "时效趋势数据") private TimeTrendData timeTrendData; - @Schema(description = "漏斗数据") - private List funnelData; + @Schema(description = "工单状态分布数据") + private List statusDistribution; @Schema(description = "热力图数据(近7天)") private HeatmapData heatmapData; @@ -45,6 +45,9 @@ public class DashboardStatsRespVO { @Schema(description = "功能类型排行") private List 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 dates; + @Schema(description = "每日排队工牌数量") + private List queueData; + } + } diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DeviceTrafficRealtimeRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DeviceTrafficRealtimeRespVO.java index ea58b02..6e50aea 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DeviceTrafficRealtimeRespVO.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DeviceTrafficRealtimeRespVO.java @@ -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; - } diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficRealtimeRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficRealtimeRespVO.java index 6ce4ffb..2969bac 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficRealtimeRespVO.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficRealtimeRespVO.java @@ -27,11 +27,11 @@ public class TrafficRealtimeRespVO { @Schema(description = "区域客流明细") private List 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; diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficTrendRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficTrendRespVO.java index 2cf07e4..d0a56b5 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficTrendRespVO.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficTrendRespVO.java @@ -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 dates; @Schema(description = "每日进入人数") diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsService.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsService.java index 290f547..5fa3ec1 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsService.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsService.java @@ -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 areaIds); + TrafficRealtimeRespVO getAreaTrafficRealtime(List areaIds, LocalDate date); /** * 按区域ID列表查趋势(汇总) diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsServiceImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsServiceImpl.java index c9a0fcf..5cec58f 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsServiceImpl.java @@ -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 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 statusDistribution = buildStatusDistribution(orderType, sevenDaysAgoStart, nowEnd); + + // 9. 热力图(近7天) HeatmapData heatmapData = buildHeatmapData(orderType, sevenDaysAgoStart, nowEnd); // 10. 功能类型排行(单次 GROUP BY 查询) List 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 todayStats = trafficStatisticsMapper.selectList( + // 从 ops_traffic_statistics 表查询指定日期汇总(使用 ge + lt 避免边界重叠) + List dayStats = trafficStatisticsMapper.selectList( new LambdaQueryWrapperX() - .ge(OpsTrafficStatisticsDO::getStatHour, todayStart) - .lt(OpsTrafficStatisticsDO::getStatHour, todayEnd)); - - // 查询昨日数据(用于昨日小时趋势对比) - LocalDate yesterday = today.minusDays(1); - LocalDateTime yesterdayStart = yesterday.atStartOfDay(); - List yesterdayStats = trafficStatisticsMapper.selectList( - new LambdaQueryWrapperX() - .ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart) - .lt(OpsTrafficStatisticsDO::getStatHour, todayStart)); + .ge(OpsTrafficStatisticsDO::getStatHour, dayStart) + .lt(OpsTrafficStatisticsDO::getStatHour, dayEnd)); // 加载区域名称映射 Map areaNameMap = loadAreaNameMap(); // 按区域汇总 - Map> byArea = todayStats.stream() + Map> 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() + .eq(OpsOrderDO::getOrderType, orderType) + .notIn(OpsOrderDO::getStatus, "COMPLETED", "CANCELLED"))); // 3. 平均响应时长(今日已完成的工单,使用 ge + lt 避免边界重叠) List 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 stats = trafficStatisticsMapper.selectList( new LambdaQueryWrapperX() @@ -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 yesterdayStats = trafficStatisticsMapper.selectList( - new LambdaQueryWrapperX() - .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 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 yesterdayStats = trafficStatisticsMapper.selectList( - new LambdaQueryWrapperX() - .eq(OpsTrafficStatisticsDO::getAreaId, areaId) - .ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart) - .lt(OpsTrafficStatisticsDO::getStatHour, todayStart)); - Map areaNameMap = loadAreaNameMap(); String areaName = areaNameMap.getOrDefault(areaId, "未知区域"); - // 按设备分组(今日) + // 按设备分组 Map> byDevice = stats.stream() .filter(s -> s.getDeviceId() != null) .collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getDeviceId)); - // 按设备分组(昨日) - Map> yesterdayByDevice = yesterdayStats.stream() - .filter(s -> s.getDeviceId() != null) - .collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getDeviceId)); - List result = new ArrayList<>(); for (Map.Entry> 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 areaIds) { + key = "T(String).join(',', #areaIds.![toString()]) + ':' + (#date != null ? #date.toString() : T(java.time.LocalDate).now().toString())", unless = "#result == null") + public TrafficRealtimeRespVO getAreaTrafficRealtime(List 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 todayStats = trafficStatisticsMapper.selectList( + // 查询指定日期数据,条件 areaId IN (areaIds) + List dayStats = trafficStatisticsMapper.selectList( new LambdaQueryWrapperX() .in(OpsTrafficStatisticsDO::getAreaId, areaIds) - .ge(OpsTrafficStatisticsDO::getStatHour, todayStart) - .lt(OpsTrafficStatisticsDO::getStatHour, todayEnd)); - - // 查询昨日数据 - LocalDate yesterday = today.minusDays(1); - LocalDateTime yesterdayStart = yesterday.atStartOfDay(); - List yesterdayStats = trafficStatisticsMapper.selectList( - new LambdaQueryWrapperX() - .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 areaNameMap = loadAreaNameMap(); - Map> byArea = todayStats.stream() + Map> 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 buildFunnelData(String orderType, LocalDateTime startDateTime, LocalDateTime endDateTime) { - // 创建工单 = 时间范围内创建的所有非取消工单 - long totalCreated = opsOrderMapper.selectCount( - new LambdaQueryWrapperX() - .eq(OpsOrderDO::getOrderType, orderType) - .ne(OpsOrderDO::getStatus, "CANCELLED") - .ge(OpsOrderDO::getCreateTime, startDateTime) - .lt(OpsOrderDO::getCreateTime, endDateTime)); + private List buildStatusDistribution(String orderType, + LocalDateTime startDateTime, + LocalDateTime endDateTime) { + Map 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() - .eq(OpsOrderDO::getOrderType, orderType) - .notIn(OpsOrderDO::getStatus, "PENDING", "CANCELLED") - .ge(OpsOrderDO::getCreateTime, startDateTime) - .lt(OpsOrderDO::getCreateTime, endDateTime)); + // 使用 selectCount 按状态逐个查询(状态数量有限,最多7次查询,避免大量数据加载到内存) + Map statusCountMap = new HashMap<>(); + for (String status : statusLabelMap.keySet()) { + Long count = opsOrderMapper.selectCount( + new LambdaQueryWrapperX() + .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() - .eq(OpsOrderDO::getOrderType, orderType) - .in(OpsOrderDO::getStatus, "ARRIVED", "COMPLETED", "PAUSED") - .ge(OpsOrderDO::getCreateTime, startDateTime) - .lt(OpsOrderDO::getCreateTime, endDateTime)); - - // 已完成 = 时间范围内创建且已完成的工单 - long completed = opsOrderMapper.selectCount( - new LambdaQueryWrapperX() - .eq(OpsOrderDO::getStatus, "COMPLETED") - .eq(OpsOrderDO::getOrderType, orderType) - .ge(OpsOrderDO::getCreateTime, startDateTime) - .lt(OpsOrderDO::getCreateTime, endDateTime)); - - List 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 result = new ArrayList<>(); + for (Map.Entry 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 orders = opsOrderMapper.selectList( + new LambdaQueryWrapperX() + .select(OpsOrderDO::getId) + .eq(OpsOrderDO::getOrderType, orderType) + .ge(OpsOrderDO::getCreateTime, startDateTime) + .lt(OpsOrderDO::getCreateTime, endDateTime)); + Set orderIds = orders.stream() + .map(OpsOrderDO::getId) + .collect(Collectors.toSet()); + + if (orderIds.isEmpty()) { + // 无工单,直接返回空数据 + List dates = new ArrayList<>(); + List 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 queueRecords = + opsOrderQueueMapper.selectList( + new LambdaQueryWrapperX() + .in(OpsOrderQueueDO::getOpsOrderId, orderIds) + .ge(OpsOrderQueueDO::getEnqueueTime, startDateTime) + .lt(OpsOrderQueueDO::getEnqueueTime, endDateTime)); + + Map dailyQueueMap = queueRecords.stream() + .filter(q -> q.getEnqueueTime() != null) + .collect(Collectors.groupingBy( + q -> q.getEnqueueTime().toLocalDate().toString(), + Collectors.summingInt(q -> 1) + )); + + List dates = new ArrayList<>(); + List 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(); + } + /** * 构建客流小时趋势 */ diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/config/StatisticsConfiguration.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/config/StatisticsConfiguration.java index dba48de..6322772 100644 --- a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/config/StatisticsConfiguration.java +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/config/StatisticsConfiguration.java @@ -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 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) { diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/traffic/OpsTrafficController.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/traffic/OpsTrafficController.java index 26f539f..28ea8eb 100644 --- a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/traffic/OpsTrafficController.java +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/traffic/OpsTrafficController.java @@ -42,23 +42,26 @@ public class OpsTrafficController { @GetMapping("/realtime") @Operation(summary = "全局实时客流监测") @PreAuthorize("@ss.hasPermission('ops:traffic:query')") - public CommonResult getTrafficRealtime() { + public CommonResult 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 getTrafficTrend() { + public CommonResult 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 getAreaTrafficRealtime( - @RequestParam("areaIds") List areaIds) { + @RequestParam("areaIds") List 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")